型推論、foreach、ラムダ式
型推論
変数を宣言する場合はint型やdouble型などのデータ型名が必要です。
しかし、与えられた値によってデータ型が推測できる場合は、記述を簡略化できます。
これを型推論といいます。
#include <iostream>
int main()
{
int numberA = 10;
double realA = 20.0;
//型推論
auto numberB = 30;
auto realB = 40.0;
std::cout << typeid(numberA).name() << std::endl
<< typeid(realA).name() << std::endl
<< typeid(numberB).name() << std::endl
<< typeid(realB).name() << std::endl;
std::cin.get();
}
int double int double
数値リテラルはデータ型が決まっているので、その値からそのデータ型を推測することができます。
このような場合は、データ型名の代わりにautoというキーワードを指定することで、自動的に適切なデータ型の変数を作ることができます。
上のようなコードでは従来通りintやdoubleと書いても同じことで、メリットはほぼありませんが、データ型名が長くなる場合などは記述が簡潔になりコードが読みやすくなります。
たとえばイテレータはデータ型名が長くなりがちなので、autoを使うことですっきりと記述できるようになります。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
for (std::vector<int>::iterator itr = vec.begin(); itr != vec.end(); ++itr)
{
std::cout << *itr << std::endl;
}
//↓auto使用
for (auto itr = vec.begin(); itr != vec.end(); ++itr)
{
std::cout << *itr << std::endl;
}
それだけでなく、vectorクラスから別のコンテナクラス(例えばlistクラス)にデータ型を変更した場合でも修正の必要がなくなるというメリットもあります。
型が推測できない場合にはautoは使用できません。
例えば初期化子がない場合などはエラーになります。
foreach
C++11では、for文の仕様が拡張され、配列やコンテナ型などのデータ集合に対する新しいループ方法が使用可能となっています。
これは他のプログラミング言語ではforeachという構文で用意されているもので、機能的にもほぼ同等です。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
//foreach
for (int v : vec)
{
std::cout << v << std::endl;
}
0 1 2 3 4
「int v」はただint型の変数の宣言をしているわけではなく、vectorのインスタンスであるvecから取り出したデータが格納されます。
forブロック中では通常のint型変数として使用可能です。
ループの終端に達したら、vecから次のデータを取り出してvに格納します。
これをvecの要素数回繰り返したらループを終了します。
従来のfor文ではループの終了条件式や反復式などの指定があり、この指定次第で複雑なループ処理が可能です。
しかし「配列の全ての要素に一度ずつアクセスする」という条件で足りる場合は、foreachの方が簡潔に書け、かつ確実なループ処理が可能です。
データ型の指定にはautoが使用可能ですから、いちいちデータ型を確認する必要はありません。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
for (auto v : vec)
{
std::cout << v << std::endl;
}
ただし、foreachでは要素を書き換えることはできません。
これは、「v」は要素への参照やポインタではなくコピーなので、書き換えても元のデータには影響しないためです。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
for (auto v : vec)
{
v *= 2;
}
for (auto v : vec)
{
std::cout << v << std::endl;
}
0 1 2 3 4
値を書き換えたい場合は参照にすれば可能です。
std::vector<int> vec{ 0, 1, 2, 3, 4 };
//&を付けて参照にする
for (auto &v : vec)
{
v *= 2;
}
for (auto v : vec)
{
std::cout << v << std::endl;
}
0 2 4 6 8
ラムダ式
ラムダ式とは、関数オブジェクトを定義するための新しい文法です。
通常の関数はグローバルな領域に「戻り値の型」「関数名」「引数」「処理ブロック」を記述することで定義しますが、ラムダ式を用いると関数ブロック内に簡易的な関数を作ることができます。
文章だけではわかりにくいと思うので以下を見てください。
#include <iostream>
int main()
{
auto func = [] { std::cout << "hello" << std::endl; };
func();
std::cin.get();
}
hello
5行目で、ラムダ式により「func」という名前の関数オブジェクトを作成しています。
その関数オブジェクトを実際に呼び出しているのは7行目です。
最小のラムダ式
最小の、「何もしない」ラムダ式は以下のようになります。
[]{};
[]はラムダ式を使用するために必要な記号ですが、ひとまず横に置いておきます。
{}は、おなじみのブロックを指定する記号で、この中に実際の処理を記述します。
通常の関数でもラムダ式による関数でも、関数を呼び出す際には関数呼び出し演算子()を使用します。
これは引数がない関数であっても省略することはできません。
よって、画面に文字を表示する関数オブジェクトを定義し、呼び出すための最小のコードは以下のようになります。
[]{ std::cout << "hello" << std::endl; }();
この一行をmain関数中に記述すれば「hello」と出力されます。
丸括弧よりも手前がラムダ式による関数の定義で、丸括弧によって今定義した関数を呼び出す、という処理になります。
このような、関数オブジェクトを識別・アクセスする名前を持たない関数を無名関数と言います。
名前を付ける
無名関数は使いどころが限られますが、名前を付ければほかの関数に近い使い方が可能になります。
名前を付けるにはautoを使用します。
auto func = [] { std::cout << "hello" << std::endl; };
func();
これは最初に例示したコードと同じです。
ラムダ式そのものをautoで宣言した変数に代入することで、式に名前を付けることができます。
autoは型推論で変数を宣言するだけの機能ですから、呼び出せるのはその変数のスコープ内からに限られます。
引数
ラムダ式に引数を指定するには、[]の後に()を記述し、そこに引数を指定します。
//無名関数
[](int x, int y){ std::cout << (x + y) << std::endl; }(5, 7);
//名前付き
auto func = [](int x, int y){ std::cout << (x + y) << std::endl; };
func(5, 7);
記述方法も呼び出し方法も通常の関数と同じです。
ちなみに、引数がないラムダ式の場合でも()を付けて定義することもできます。
[](){ std::cout << "hello" << std::endl; }();
戻り値
ラムダ式は戻り値を指定することもできます。
//戻り値の型推論
auto addA = [](int x, int y) { return x + y; };
//戻り値を明示的に指定
auto addB = [](int x, int y) -> short { return x + y; };
std::cout << addA(5, 7) << std::endl;
std::cout << addB(2, 3) << std::endl;
return文に戻り値を指定すれば、呼び出し元でその値を得ることができます。
引数の指定の丸括弧の後にアロー演算子(->)とデータ型を指定することで、戻り値の型を明示的に指定することもできます。
変数のキャプチャ
ラムダ式は特定の関数内に記述されますが、ラムダ式のブロック内は独立したスコープとなっており、ラムダ式外の変数にアクセスすることはできません。
int main()
{
int number = 10;
//変数numberにアクセスできないためエラー
[] { std::cout << number << std::endl; }();
}
ブロック外の変数にアクセスするには引数で受け取るほか、変数のキャプチャという機能を使用することもできます。
#include <iostream>
int main()
{
int number = 10;
//コピー
[=] { std::cout << number << std::endl; }();
//参照
[&] { number *= 2; }();
std::cout << number << std::endl;
std::cin.get();
}
10 20
ラムダ式の[]の中に=記号を指定すると、その時点までに宣言されている変数をラムダ式内で使用することができます。
この時、変数のコピーがラムダ式内で得られます。
[]の中に&記号を指定すると、変数への参照を得ることができます。
参照ですから、ラムダ式内で値を書き換えると元の変数にも影響します。
キャプチャのタイミング
変数のキャプチャは、コピーと参照とでは値をキャプチャするタイミングが異なります。
コピーの場合は「ラムダ式を定義した時点での値」がキャプチャされます。
参照の場合は「ラムダ式を実行する直前の値」がキャプチャされます。
(というより参照なので、常に元の変数と同じ値になる)
#include <iostream>
int main()
{
int number = 0;
auto f1 = [=] { std::cout << "コピー: " << number << std::endl; };
auto f2 = [&] { std::cout << "参照 : " << number << std::endl; };
number = 10;
f1();
f2();
std::cin.get();
}
コピー: 0 参照 : 10
mutable
変数のキャプチャに[=]を指定した時、コピーされた値を得られますが、この変数は書き換えることはできません。
変数を書き換える場合は引数の後にmutableを指定します。
コピーであることには変わりはないので、ラムダ式内で書き換えても元の変数には影響しません。
int number = 10;
[=]() {
//number *= 2; //これはエラー
std::cout << number << std::endl;
}();
//mutable指定
[=]() mutable {
number *= 2; //書き換え可能
std::cout << number << std::endl; //20
}();
//ラムダ式外には影響しない
std::cout << number << std::endl; //10
mutableを指定する場合、引数がないラムダ式であっても引数指定の()を省略することはできないので注意してください。
//エラー
[=] mutable {}();
キャプチャする変数の指定
キャプチャは、それまでに宣言されているすべての変数が対象になりますが、個別に指定することもできます。
class TestClass
{
int private_num = 10;
public:
int public_num = 20;
void func()
{
//自分自身のインスタンスを受け取る
//メンバ変数にアクセス可能
[this]
{
//privateメンバにもアクセス可能
std::cout << private_num << std::endl;
std::cout << public_num << std::endl;
}();
}
};
int main()
{
int a = 10, b = 20, c = 30;
//aをコピー、bを参照で受け取る
//cにはアクセスできない
[a, &b]() {}();
//cのみ参照で受け取る
//その他はコピー
[=, &c]() {}();
//cのみコピーで受け取る
//その他は参照
[&, c]() {}();
}
クラス内でのthisはポインタであるため、参照でもコピーでも値を書き換えるとメンバ変数が書き換わります。
ラムダ式を引数で受け取る
テンプレートを使用することで、ラムダ式を引数として受け取る関数を作ることができます。
template<typename Func>
void test(Func f)
{
f(); //「hello」を表示
}
int main()
{
//引数にラムダ式を指定
test([] {std::cout << "hello" << std::endl; });
}
関数ポインタへの変換
ラムダ式は、同じ引数と戻り値を持つ関数ポインタに変換することができます。
ただし変換できるのは変数をキャプチャしないラムダ式に限られます。
#include <iostream>
int main()
{
//引数にint型ひとつ、戻り値がint型の
//fpという名前の関数ポインタ
int (*fp)(int);
//同じシグネチャのラムダ式を代入
fp = [](int a) { return a * a; };
int r = fp(2);
std::cout << r << std::endl;
std::cin.get();
}
4
これを利用することでも関数の引数にラムダ式を直接指定することが可能です。
void test(int(*fp)(int))
{
int r = fp(2);
std::cout << r << std::endl;
}
int main()
{
test([](int a) { return a * a; });
std::cin.get();
}
std::functionで受け取る
std::functionを使用することでもラムダ式を変数や引数に格納することができます。
使用するには#include <functional>が必要です。
#include <iostream>
#include <functional>
//short型引数ひとつ、戻り値はint型
void test(std::function<int(short)> f)
{
int r = f(2);
//20
std::cout << r << std::endl;
}
int main()
{
int a = 10;
//キャプチャ可能
test([=](short x) { return a * x; });
std::cin.get();
}
std::functionは関数ポインタでもラムダ式でも格納できるのでとても便利です。