こんにちは。田原です。
前回、std::tupleの各要素を動的にアクセスする方法を解説しました。しかし、取り出した要素に対して行う処理は別途 関数テンプレートを用意して呼び出しました。しかし、その内容が数行程度の短い時は別のところで定義するのではなくアクセスしているコードの近くで書きたいものです。(ひと目で見れたほうが読みやすいですよね。)そのためにジェネリック・ラムダ式が便利です。今回は、その第1段階として関数オブジェクトとラムダ式の基本を解説します。
1.関数オブジェクトとラムダ式
ラムダ式は一種の関数オブジェクトです。「名無しの関数オブジェクト」とも言えます。
1-1.関数オブジェクトとは
これは「関数呼び出し演算子operator()
が実装されているクラス」のインスタンスです。
使い方としては結構特殊ですので、まずは関数呼び出し演算子ではなくて普通のメンバ関数で書いてみます。
#include <iostream> class foo { int mData; public: foo() : mData(0) { std::cout << "foo::foo() : mData=" << mData << "\n"; } void add(int iData) { mData += iData; std::cout << "foo::add(" << iData << ") : mData=" << mData << "\n"; } }; int main() { foo aFoo; aFoo.add(1); }
関数呼び出し演算子は、戻り値 operator()(仮引数リスト) { 処理; }
の書式で定義します。
上記add()メンバ関数と同じことをするoperator()
を追加してみます。単純に"add"
の部分を"operator()"
と書き換えた関数を追加するだけです。
void operator()(int iData) { mData += iData; std::cout << "foo::operator()(" << iData << ") : mData=" << mData << "\n"; }
このaFooが関数オブジェクトです。次のようにして呼び出せます。
aFoo.operator()(20);
しかし、この呼び出し方では「演算子オーバーロード」の名がすたりますね。実際にはこれは関数っぽく使えるようにする演算子ですから、次のように使えます。
aFoo(300);
foo::foo() : mData=0 foo::add(1) : mData=1 foo::operator()(20) : mData=21 foo::operator()(300) : mData=321
今回はサンプルでしたのadd()関数も残しましたが、operator()
だけを定義する場合がほとんどです。
1-2.関数オブジェクトと関数の違い
クラスを関数のように見せているものですから普通の関数と異なることは分かると思いますが、他にも様々な相違点があります。その相違をリストしてみます。
- メンバ変数を持てます
そもそもクラスと関数の最大の相違はここです。普通の関数(グローバル関数やstaticなメンバ関数)はグローバル変数やローカル変数しか取り扱えませんが、クラスのメンバ関数は更にワンセットのデータ群(メンバ変数)も取り扱える複数の関数を書けます。 -
関数の中で定義できます
C++は関数の中でローカルな関数を定義できませんが、ローカルなクラスは定義できます。つまり関数オブジェクトを関数内でローカルに定義できます。ただし、クラス・テンプレートやメンバ関数テンプレートを持つクラスは残念ながら定義できません。 -
テンプレートの仕組みが異なります
- 関数テンプレート
非テンプレートによるオーバーロード、もしくは、明示的特殊化により同じ名前で異なる関数を定義できます。
テンプレート形式のままでもオーバーロードできます。これはクラスの部分的特殊化相当の機能です。クラスと異なり部分特殊化はできません。
型推論によりテンプレート実引数を明示的に指定しないで実体化できます。(暗黙的実体化) - クラス・テンプレート
そもそもオーバーロードはありません。(*1)
しかし、明示的特殊化や部分的特殊化で同じ名前で異なるクラスを定義できます。
(C++14まで)型推論に対応していないので実体化する時はテンプレート実引数を明示的に指定する必要があります。
- 関数テンプレート
(*1)クラスにオーバーロードはない
関数は引数の型が異なると同じ名前で異なる関数をオーバーロードできますが、クラス自体に引数は存在しません。コンストラクタの引数があるのでこれを使ってオーバーロードする仕様も考えられますが、C++はコンストラクタ自体のオーバーロードに使われていますので、クラスのオーバーロード定義はできません。
1-3.関数内で関数オブジェクトを定義
C++はローカル関数を定義できませんが、前述のようにローカル・クラスなら定義できます。
ちょっとやってみましょう。
#include <iostream> int main() { class foo { int mData; public: foo() : mData(0) { std::cout << "foo::foo() : mData=" << mData << "\n"; } void operator()(int iData) { mData += iData; std::cout << "foo::operator()(" << iData << ") : mData=" << mData << "\n"; } } aFoo; aFoo(300); }
foo::foo() : mData=0 foo::operator()(300) : mData=300
クラス名のfooと関数オブジェクト名のaFooの2つの名前が必要になります。このような数行程度の短い処理に一々名前を付けるのも面倒です。
因みに、aFooをfooにすることも可能ですが、それ以降、クラスfooを使って別の関数オブジェクトを定義できなくなります。
#include <iostream> int main() { class foo { int mData; public: foo() : mData(0) { std::cout << "foo::foo() : mData=" << mData << "\n"; } void operator()(int iData) { mData += iData; std::cout << "foo::operator()(" << iData << ") : mData=" << mData << "\n"; } } foo; foo(300); // foo aFoo2; // fooはローカル変数なので、その後に謎のaFoo2があるとみなされエラー }foo::foo() : mData=0 foo::operator()(300) : mData=300
2.そこでラムダ式です
ラムダ式は匿名関数とも呼ばれるように名前無しでも使えます。それに近づけるためにまずは先程のfooの名前を減らします。
2-1.名前無しローカル・クラスで関数オブジェクト
C++ではクラス名を省略することもできます。ただし、コンストラクタはクラス名で指定するのでクラス名を省略するとコンストラクタを書けません。そこで、コンストラクタと(初期化処理を書けないので)メンバ変数を削除して定義します。
#include <iostream> int main() { struct { void operator()(int iData) { std::cout << "foo::operator()(" << iData << ")\n"; } } foo; foo(300); }
foo::operator()(300)
さて、クラス名を付けていないのでこのfooは関数オブジェクトが入る変数名です。メンバ変数がないため、初期化する必要はありません。(といいつつ、実はデフォルト・コンストラクタが自動生成されています。)
2-2.ラムダ式にしてみる
2-1の関数オブジェクト変数fooと同様なものをラムダ式にしてみます。
修正手順は単純です。皮のstruct { }
、変数名fooを削除し、void operataor()
の部分を[]
へ書き換えます。
変数名を省略したということは、例えば、0.123+0.456;のような式だけを書いたようなものです。変数へ代入していないわけです。つまり、ラムダ式は値(右辺値)ということになります。
struct { void operator()(int iData) { std::cout << "foo::operator()(" << iData << ")\n"; } } foo;
上述した修正手順により、次のように修正できます。
[](int iData) { std::cout << "foo::operator()(" << iData << ")\n"; } ;
インデントや改行を整理し、出力文字列を実態に合わせると、次のようになります。
[](int iData) { std::cout << "[](" << iData << ")\n"; };
先に述べたようにこれは式を書いただけです。代入していませんから、0.123+0.456;
と書いたのと同じく、これだけでは何の意味もありませんね。しかし、このまま直接呼び出すことができますのでやってみます。
[](int iData) { std::cout << "[](" << iData << ")\n"; }(300);
まとめると
#include <iostream> int main() { [](int iData) { std::cout << "[](" << iData << ")\n"; }(300); }
[](300)
この5行目~8行目までのラムダ式の定義と呼び出しは1つ文です。5行目には仮引数(int iData)を書いています。そして8行目には実引数(300)を書いています。つまり1つの文の中で仮引数の定義と実引数を指定した呼び出しを同時にやっています。
不思議な使い方ですね。
2-3.ラムダ式を保持する
さて、ラムダ式を変数などに保持できないと不便です。しかし、どんな型なのか解らないので変数を定義出来ない気がしますね。あの強力なdecltypeを使ってtypedefしたいところですが、直接はできません。(*2)
error: lambda-expression in unevaluated context typedef decltype([](int iData){std::cout << "[](" << iData << ")\n";}) lambda;
だそうです。
しかし、ラムダ式と同時にC++11で導入された auto を使えば変数に代入できます。autoは初期化付きの宣言文の「型名」の部分に記述でき、初期値の型により「型推論」にて決定されます。
#include <iostream> int main() { auto foo=[](int iData){std::cout << "[](" << iData << ")\n";}; foo(300); foo(200); foo(100); }
関数テンプレートの型推論を使って実引数として渡すこともできます。
#include <iostream> template<class tFunction> void bar(tFunction iFunction) { iFunction(12345); } int main() { bar([](int iData){std::cout << "[](" << iData << ")\n";}); }
実はこの使い方は強力で、関数呼び出し(上記のiFunction(12345);
)がエラーにならないなら何でも渡せます。
#include <iostream> template<class tFunction> void bar(tFunction iFunction) { iFunction(12345); } void test(int iData) { std::cout << "test(" << iData << ")\n"; } int main() { // ラムダ式 bar([](int iData){std::cout << "[](" << iData << ")\n";}); // 関数ポインタ bar(test); // 関数オブジェクト struct { void operator()(int iData) { std::cout << "foo::operator()(" << iData << ")\n"; } } foo; bar(foo); }
[](12345) test(12345) foo::operator()(12345)
最後に、C++14で導入された関数の戻り値の型推論を使えば、ラムダ式を返却する関数を作ることができます。
#include <iostream> auto baz() { return [](int iData){std::cout << "[](" << iData << ")\n";}; } int main() { auto foo = baz(); foo(56789); }
[](56789)
(*2)ラムダ式を直接decltypeできない
ですが、間接的にはできます。#include <iostream> #include "typename.h" int main() { auto foo=[](int iData){std::cout << "[](" << iData << ")\n";}; foo(300); std::cout << sizeof(foo) << "\n"; typedef decltype(foo) lambda; std::cout << TYPENAME(lambda) << "\n"; // VC++ : error C3497: ラムダのインスタンスは作成できません // g++ : a lambda closure type has a deleted default constructor // lambda aLambda; }[](300) 1 class <lambda_738d863126c89c5fad3e8226329a2f74>[](300) 1 main::{lambda(int)#1}しかし、コメントアウトしている行を有効にしてみると分かりますが、デフォルト・コンストラクタがないため、生成することはできないようです。
3.まとめ
今回は関数オブジェクトとラムダ式の基本について解説しました。
勘違いしやすいのですが、関数オブジェクト同様ラムダ式はクラスではありません。operator()
を持つクラス型の値(=インスタンス=オブジェクト)です。型ではなく値(右辺値)です。
ところで、関数オブジェクトはメンバ変数を保持できます。では、ラムダ式はというと実はメンバ変数を持てます。といいますか、キャプチャという仕組みのためにメンバ変数を使っていると言っても良いでしょう。そこで、次回はこのキャプチャについて主に解説したいと思います。お楽しみに。