C++14では、通常の関数に、戻り値の型推定という機能が加わった。
C++14の戻り値の型推定を解説する前に、まずC++11の話を使用。
戻り値の型推定は、C++11のlambda式に備わっていた。ただし、C++11では、lambda式が戻り値の型推定を行うためには、本体はreturn文ひとつだけでなければならないという制約があった。return文ひとつだけでない場合、戻り値の型はvoid型となる。
// C++11のlambda式
// 戻り値の型はint
[](){ return 0 ; } ;
// 戻り値の型はvoid
[]() { } ;
// ill-formed.
// 戻り値の型はvoidなのにint型を返している。
[]()
{
int x = 0 ;
return x ;
} ;
C++14では、通常の関数の戻り値の型推定機能の導入と共に、そのような制限は撤廃された。
// C++14のlambda式
// well-formed
[]()
{
int x = 0 ;
return x ;
} ;
また、C++11では、placeholder typeという機能が導入された。
// C++11のplaceholder type
auto x = 0 ; // int
C++14では、placeholder typeに、decltype(auto)が追加された。
// C++14のplaceholer type
decltype(auto) x = 0 ; // int
decltype(auto)とautoの違いについては、前回の解説を参照。
本の虫: C++14の新機能: decltype(auto)
C++14に追加された通常の関数の戻り値の型推定は、このplaceholder typeを関数の戻り値の型として指定できる。その場合、型はreturn文のオペランドの式から推定される。
// C++14の戻り値の型推定
// void
auto f() { }
// int
auto g() { return 0 ; }
// int
decltype(auto) h() { return 0 ; }
戻り値の型としてのplaceholder typeは、後置することもできる。
auto f() -> auto
{
return 0 ;
}
この文法が認められている理由は、主に、lambda式にplaceholder typeを記述するためだ。
// int &
[]() -> auto &
{
static int x ;
return x ;
} ;
decltype(auto)は、主に戻り値の型推定を行わせるために追加された。
int & f() ;
// int
auto g() { return f() ; }
// int &
decltype(auto) h() { return f() ; }
C++14の戻り値の型推定には、C++11のlambda式の戻り値の型推定のような、とても使いづらい制限はない。普通に書けば普通に動く。ただし、細かい点で注意すべきところはある。
すべてのreturn文のオペランドの式の型は、一致していなければならない。
auto f()
{
return 0 ;
return 0.0 ; // ill-formed. 型の不一致
}
再帰はできる。もちろん、return文のオペランドの式の型は、すべて一致していなければならない。
auto ackermann( int m, int n )
{
if ( m == 0 )
return n + 1 ;
if ( n == 0 )
return ackermann( m - 1, 1 ) ;
else
return ackermann( m - 1, ackermann( m, n-1 ) ) ;
}
placeholder typeの型推定にあたって、まだ型推定されていないplaceholder typeが現れる場合は、エラーとなる。これは、変数の場合と同等だ。
auto x = x ; // エラー
auto f() ;
// エラー
auto g() { return f() ; }
placeholder typeを使った関数を再宣言する場合は、同じplaceholder typeを使わなければならない。推定される具体的な型を使うことはできない。
auto f() ; // OK、宣言
auto f() { return 0 ; } // OK、定義
auto f() ; // OK、再宣言
// ill-formed
// 同じplaceholer typeを使わなければならない
decltype(auto) f() ;
int f() ;
関数テンプレートの明示的実体化や明示的特殊化でも、同じplaceholder typeを使わなければならない。
// #1
template < typename T > auto f( T t ) { return t ; }
// 明示的実体化
extern template auto f( int ) ; // OK
extern template char f( char ) ; // エラー
// 明示的特殊化
template < > auto f( short ) ; // OK
template < > long f( long ) ; // エラー
// #2
// #2は#1とは異なるテンプレートであることに注意
template < typename T > T f( T t ) { return t ; }
extern template char f( char ) ; // OK, #2の明示的実体化
template < > long f( long ) ; // OK、#2の明示的特殊化
もちろん、上記のテンプレートは、実際に実体化して使う際に曖昧になるが、それは使う場合の話であって、テンプレートの話ではない。
virtual関数で戻り値の型推定機能を使うことはできない。提案論文のN3638によれば、技術上可能ではあるが、オーバーライドのチェックとvtableレイアウトが複雑化するため、現時点では禁止しておくとのことだ。
戻り値の型推定で、std::initializer_listを推定することはできない。
placeholder typeで変数を宣言する場合は、std::initalizer_listを推定することができるが、std::initializer_listは実装の都合上、自動ストレージ上に構築して参照渡しをするため、関数の戻り値として返すのは不適切であるという理由に寄る、
// OK
// std::initializer_list<int>
auto x = { 1, 2, 3 } ;
// エラー
// std::initializer_listは推定できない
auto f()
{
return { 1, 2, 3 } ;
}
以上、細かい点はあるが、通常の利用では、それほど気にする必要はない。普通に書けば、自然に動くように設計されているはずだ。
See Also:
本の虫: C++14の新機能: decltype(auto)
ドワンゴ広告
ドワンゴは本物のC++プログラマーを募集しています。
CC BY-ND 4.0: Creative Commons — Attribution-NoDerivatives 4.0 International — CC BY-ND 4.0
大変勉強になりました。
ReplyDelete関数や多相ラムダもだいぶん使いやすくなったようですね。
とはいえ、戻り型が可変じゃないならあんまりautoは使わないと思います。可読性の面で劣るからです。
しかし、autoを使うことで将来的に大きな型へ自動対応できるあたりは便利そうですね。
バランスが難しいです。自明なコードを書くように心がけたいところです。
C++では戻り値の型は静的に決まっていて、動的な振る舞いをさせたいのであれば、ポリモーフィック型を返すべきでしょう。
ReplyDelete今ならば、BoostのAnyやVariantなどのライブラリも充実しています。
お返事ありがとうございます。
ReplyDelete元々BASIC畑で育ったので、変数の扱いはバリアントのほうが得意なのです。
なので、Anyが標準に入るのを心待ちにしているのです。タイプイレーザーはまだでしょうか。
ポリモーフィックな型を返すとなるとどうしてもGCオブジェクトを返すことになるのでそれはそれで重たいなーと思います。
ポリモーフィズム自体は好物です。が、C++のそれはちと手間がかかってめんどくさいですね。
無いならないでそういう使い方をしたいです。
自分はデフォルト使用主義者なので、あんまり拡張部分は入れたくないです。今のところ。
とはいえ、多相ラムダもだいぶん使いやすくなって、ソートの記事と相まってもっとパワフルに使いやすくなっていくのが楽しいです。
いつもお世話になっています。
応援しています。がんばってください。