C++14の新機能: decltype(auto)
さて、前回に引き続き、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