さて、前回に引き続き、C++14の新機能を解説していく。今回は、decltype(auto)を解説する。
1C++14で追加されたdecltype(auto)は、C++11で追加されたauto指定子と同じ、placeholder specifierである。decltype(auto)は、autoでは解決できない問題を解決するために導入された。導入のきっかけは、これまたC++14で追加された、関数の戻り値の型推定機能のためである。これは後日解説する。
decltype(auto)は、わかりやすいといえばわかりやすい。autoのような型が勝手に変わる罠がないからだ。わかりにくいといえばわかりにくい。C++の型システムに深く関わるし、autoのようによきにはからってコンパイルが通ることがないからだ。
さて、C++14のdecltype(auto)について解説する前に、まずC++11のautoについて解説しよう。
auto指定子は変数宣言の型指定子に記述した場合、変数の型は、初期化子から決定される。
auto a = 0 ; // int
auto b = 0.0 ; // double
std::vector<int> v ;
auto iter = v.begin() ; // std::vector<int>::iterator
なるほど、一見すると、とてもわかりやすい。C++では、式の型はコンパイル時に決定できるので、このコードは危険ではないし、実行時オーバーヘッドなども一切ない。以下のように書いた場合と同等である。
int a = 0 ;
double b = 0.0 ;
std::vector<int> v ;
std::vector<int>::iterator iter = v.begin() ;
さて、単にauto指定子を使うだけならば、この程度の理解でもよい。しかし、auto指定子、というよりもC++の型システムは複雑なのだ。
「変数の型は、初期化子から決定される」と書いた。初期化子(initializer)とは、= の右側の式のことだ。
auto variable = initializer ;
「初期化子から決定される」という説明は、あまりにも物事を簡略化しすぎている。より正確に説明すると、テンプレート実引数推定のルールで型推定が行われる
つまり、こういうことだ。
template < typename T >
void f( T ) ;
// 変数variableの型はT
f( initializer ) ;
さきほどの変数variableの型は、あたかも上記のように書いた場合にテンプレート実引数推定の結果のTの型になる。
それの何が問題なのか。テンプレート実引数推定には、型の変換が入るのだ。
int array[10] = { } ;
// decltype(array) = int [10]
// decltype(x) = int *
auto x = array ;
void function() ;
// decltype(function) = void ()
// decltype(y) = void (*)()
auto y = function ;
int const const_obj = 0 ;
// decltype(const_obj) = int const
// decltype(z) = int
auto z = const_obj ;
特に問題になるのは、リファレンスだ。
int obj = 0 ;
int & ref = obj ;
// decltype(ref) = int &
// decltype(x) = int
auto x = ref ;
lvalueリファレンスの場合は、以下のように書くという手もある。
auto & x = ref ;
残念ながら、rvalueリファレンスの場合は、この方法は使えない。
int & l() ;
int && r() ;
// ん?
// decltype(x) = int &
auto && x = l() ;
// decltype(y) = int &&
auto && y = r() ;
変数xの型はlvalueリファレンスになる。これは、auto指定子がテンプレート実引数推定のルールを利用しているためだ。そのルール上、初期化子の型から、変換がかかる。
これを防いで、初期化子の型をそのまま使うには、decltypeを使う方法がある。
decltype(expr) x = expr ;
なるほど、たしかにこれは動く。問題は、コード中にexprが重複してしまうということだ。同じコードを機械的に二度書かねばならないし、問題の元だ。
このために、C++14には、初期化子の型をそのまま使う、decltype(auto)が追加された。
int && f() ;
auto x = f() ; // int
decltype(auto) y = f() ; // int &&
decltype(auto)は、autoを初期化子の式で置き換えたかのように振る舞う。つまり、初期化子の型になる。
auto指定子との挙動の違いを比較してみよう。
int array[10] = { } ;
// ill-formed.
// 配列の初期化子に配列は使えない
decltype(auto) x = array ;
void function() ;
// ill-formed.
// 関数型の変数を宣言することはできない
decltype(auto) y = function ;
int const const_obj = 0 ;
// well-formed
// decltype(z)はint const
decltype(auto) z = const_obj ;
また、初期化子の式をdecltypeの中に入れるということは、初期化子が括弧で囲まれていた場合、型が変わる。
int obj = 0 ;
// decltype(x) = int
decltype(auto) x = obj ;
// decltype(y) = int &
decltype(auto) y = (obj) ;
これはdecltypeの仕様である。
See Also:
ドワンゴ広告
この記事はドワンゴ勤務中に書かれた。毎週金曜日の夜はボドゲと相場が決まっている。
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0
自分はインターフェース指向の人間なのでこれにお世話になることは少ないと思います。
ReplyDeleteむしろ基底の型を推論してくれる方がありがたかったりすることもありそうです。
まぁ、そういうあいまいなときは実物を記述しますけど。
意外とauto&が便利なので重宝しています。
タプルの要素参照なんかはまさにうってつけの素材ですよ。
参照用変数は最適化で消えますし。
auto b = 0.0 ; // double
ReplyDeleteに対応する行が
int b = 0.0 ;
になってますよ
とてもわかりやすい説明だった
ReplyDelete