C++ で Non-static data member initializers に auto が使えないのがつらい
C++14(17) になっても Non-static data member initializers に auto
が使えないのがつらい、という話です。
[Non-static data member initializers とは]
Non-static data member initializers とは C++11 から追加された言語機能の一つで クラスのメンバ変数を定義する時に初期値を設定できる という機能です。
C++03 では以下のようにコンストラクタでメンバ変数の初期値を設定することが出来ました。
struct X{ // コンストラクタ時に初期値を設定する X() : value(0) , value2(3.14f){} int value; float value2; };
一方、C++11 以降ではメンバ変数定義時に直截値を代入することが出来ます。
struct X{ // メンバ変数に対して直接初期値を代入する事が出来る // コンストラクタを定義する必要がない int value = 0; float value2 = 3.14f; };
これによりコンストラクタを定義する事なく、メンバ変数の初期値を設定する事が出来ます。
[Non-static data member initializers で auto
を使うことが出来ない]
さて、今の時代、auto
を使って型推論を行う時代ですね。
C++14 では戻り値型を auto
で型推論してくれたり、ラムダ式の引数に auto
を使うことで多相ラムダを実現することが出来るようになりました。
しかし、残念なことに Non-static data member initializers では auto
を使うことが出来ません。
struct X{ // 変数を定義する時に auto を使うのは至極当然…だが… // error: 'auto' not allowed in non-static struct member auto value = 0; auto value2 = 3.14f; };
なんで C++14 では戻り値型に auto
を指定したり、ラムダ式の引数にさえ auto
で型推論してくれるようになったのに!!
Non-static data member initializers では!!
auto
が!!!!
使えない!!!!!
なぜだ!!!!
なぜだ!!!!!!!!!
なぜ auto
が使えないのか理解に苦しむ
[Non-static data member initializers で auto
を使えないと何が困るのか]
さて、先ほどの例だけだと『別に型推論しなくたって普通に型を定義すればいいじゃん』と思うでしょう。 では、次の例だとどうでしょう。
struct X{ // ラムダ式をメンバ変数で保持したいよね!!! auto twice = [](int a){ return a + a; }; };
はいーラムダ式ーきたー 。
ラムダ式は定義された時に『ユニークな型』となるため、decltype()
のようなものを使っても定義時に型を指定することが出来ません。
[キャプチャしてないラムダ式を関数ポインタ型にキャストする]
と、ここで C++ に精通している方ならすでに気づいていると思いますが『キャプチャしてないラムダ式は関数ポインタ型にキャストすること』が出来ます。
ですので、上記の場合は以下のように記述することが出来ます。
struct X{ // キャプチャしてないラムダ式は関数ポインタ型にキャストされる!! std::common_type<int(*)(int)>::type twice = [](int a){ return a + a; }; };
やったね!!! と、思いますがこれもまだ不完全です。
[キャプチャしたラムダ式を std::function
で保持する]
例えば『ラムダ式内で this
を参照したい』場合には this
をキャプチャする必要があるので関数ポインタ型にキャストすることが出来ません。
struct X{ // this をキャプチャしたいんだけどなー… // error: no viable conversion from 'X::(lambda at ...)' to 'std::common_type<int (*)(int)>::type' (aka 'int (*)(int)') std::common_type<int(*)(int)>::type twice = [this](int a){ return a + a + offset; }; int offset = 42; };
なるほど???
が、これも std::function
を使うことで回避することが出来ます。
#include <functional> struct X{ // こういう時の std::function std::function<int(int)> twice = [this](int a){ return a + a + offset; }; int offset = 42; };
std::function
を使うことで多少オーバーヘッドが生じますがまあしょうがないですね。
[多相ラムダの場合]
さて、今までの回避方法はC++11 では有効な手段でした。
しかし、時代は C++14、もうすぐ C++17 もやってきます。
そう、多相ラムダです!!!
今の時代、ラムダ式の引数も型推論を行いたいですよね???
struct X{ // 関数ポインタ型にキャストされるけど、int 型で固定されてしまう… std::common_type<int(*)(int)>::type twice = [](auto a){ return a + a; }; };
本来は多相ラムダとして定義して twice(42)
や twice(3.14)
、twice(std::string("homu"))
など引数型に依存しないように使いたいですよね?
しかし、上記の場合では『関数ポインタ型を決定する時に引数型を決定する必要がある』ので型推論を行うことが出来ません。
この問題は std::function
を使っても同様です。
[キャプチャを行わない場合の回避方法]
回避方法としては一度、メンバ変数以外で定義してからメンバ変数に代入するということは出来ます。
struct X{ // 一度、メンバ変数以外で定義して static constexpr auto twice_ = [](auto self, auto it) constexpr{ return it + it; }; // その変数をメンバ変数として代入する decltype(X::twice_) twice = X::twice_; };
しかし、この場合は this
をキャプチャすることが出来ないのであまりメンバ変数として保持する意味がありません…。
[メンバ関数テンプレートを使う]
そもそも『メンバ変数ではなくてメンバ関数テンプレートとして定義すればいいのでは?』と思う人が多いと思います。
全く持ってそのとおりで通常は素直にメンバ関数テンプレートを定義すれば解決します。
struct X{ // 余計なメンバ変数も定義されないし、this も参照できるし完璧 template<typename T> constexpr auto twice(T t) const{ return t + t + offset; } int offset = 42; };
[メンバ関数テンプレートをオブジェクトとして扱いたかった]
さて、本題というかやりたかったことですが、単純に『メンバ関数テンプレートをオブジェクトとして扱いたかった』からです。
例えば、次のようなことは本来 C++ では行うことは出来ません。
struct X{ template<typename T> constexpr auto twice(T t) const{ return t + t + offset; } int offset = 42; }; X x{}; // x.twice をオブジェクトとして扱いたかったが… // error: reference to non-static member function must be called auto x_twice = x.twice; // x.twice を呼び出す x_twice(42);
上記のように『メンバ関数をオブジェクトとして扱う』場合には関数を『メンバ関数として定義する』のは不向きです。
そしてこれは『メンバ関数をラムダ式として定義する』ことで実現する事が出来ます。
struct X{ // 本来は auto で定義したい… // auto twice = [this](auto it){ std::function<int(int)> twice = [this](auto it){ return it + it + this->offset; }; int offset = 42; }; X x{}; std::cout << x.twice(42) << std::endl; // x.twice をオブジェクトとして扱える auto x_twice = x.twice; // x.twice を呼び出す std::cout << x_twice(12) << std::endl;
このようなことを実現したい場合は Non-static data member initializers で auto
を使うしかないのです…。
[まとめ]
C++14 や C++17 ではあちこちで型推論ができようになったのに Non-static data member initializers で auto
が使えない仕様は控えめにいってクソだと思いました。