結論を書くと,別に const
は消えていない. 単に T
が int const&
に推論されているだけだ.
// 型名のデマングル用 utility // thanks to http://cpplover.blogspot.com/2010/03/gcc.html #include <cxxabi.h> #include <cstdlib> // std::free のために追加 class Demangle { private : char * realname ; public : Demangle( std::type_info const & ti ) { int status = 0 ; realname = abi::__cxa_demangle( ti.name(), 0, 0, &status ) ; } Demangle( Demangle const & ) = delete ; Demangle & operator = ( Demangle const & ) = delete ; ~Demangle() { std::free( realname ) ; } operator char const * () const { return realname ; } } ; // 参照は typeid では無視されるので,ラップするためのテンプレート template<class T> struct type {}; // 本題 #include <iostream> #include <utility> template< class T > void f( T && ) { std::cout << Demangle( typeid(type<T>) ) << std::endl; } int main() { // rvalue f(0); // non-const lvalue int i = 0; f(i); // const lvalue int const j = 0; f(j); // おまけ: const rvalue (実用上の意味はない) f( std::move(j) ); }
type<int> type<int&> type<int const&> type<int const>
だから, template
の内部で x
を書き換えようとすればエラーになるし,
f
の中で別の関数 g(x);
を呼べば,きちんと const
としてオーバーロード解決が行われる.
#include <iostream> void g( int& ) { std::cout << "non-const\n"; } void g( int const& ) { std::cout << "const\n"; } template< class T > void f( T && x ) { // x = 0; // const の場合には呼べない g(x); // 渡した引数により正しく g が呼ばれる. // なお本来は std::forward<T>(x) とするべき( x は必要に応じて move される) } int main() { int i = 0; f(i); int const j = 0; f(j); }
non-const const
この辺りの動作は, 本の虫: rvalue reference 完全解説 の Perfect Forwarding の項が詳しい.*1
要するに, T&&
によって型推論が行われる場合, T
は参照も推論されうる,ということである.
これは std::forward
を使った Perfect Forwarding を行うために用意された機能で,
何故そのような一見して分かりにくい仕様になっているかといえば, Perfect Forwarding を行わない場合には,
- 受け取った引数に対して書き換えを行い,書き換えを行った結果は無視したい場合*2には,
template
のように,値渡しを行えば良いvoid f( T x ); - 受け取った引数に対して書き換えを行い,書き換えを行った結果を引数経由で戻したい場合には,
template
のように,(いわゆる)参照渡しを使えば良いvoid f( T& x ); - 受け取った引数に対して書き換えを行わない場合には,
template
のように,void f( T const& x ); const
参照渡しを使えば良い - Rvalue Reference のみを束縛する(つまり変数を束縛できない)関数は,有ったところで使い道が滅多にない
と,このような理由から, T&&
は Perfect Forwarding の為に使うのが妥当であるからだ.
余談であるが,もし T
に対して参照を推論させたくない場合,つまり純粋に Rvalue Reference のみを束縛したい場合には, std::is_reference
で SFINAE を行えば良い.
template< class T class = typename std::enable_if<!std::is_reference<T>::value>::type > void f( T && x );
とはいえ, rvalue reference のみを束縛したい(つまり変数を渡した場合にはコンパイルエラーにしたい)ケースは,実用上では僅かなので,((もちろん,皆無というわけではないので,必要になったら遠慮せず SFINAE しよう. その場合には const
参照渡しを行う関数も用意して,変数も問題なく渡せるようにすること.))
普通は値渡しか参照渡しか const
参照渡しの いずれかを使うのが良いだろう.
おまけ
純粋な Rvalue Reference の場合のオーバーロードがあると便利なケースは,例えば以下のような状況が考えられる.
// take: 先頭 n 要素を得る. なお,実際には Concept Check を行うべき. // rvalue が渡された場合(引数を好きに書き換えて良い) template< class T, class = typename std::enable_if<!std::is_reference<T>::value>::type > T take( int n, T && x ) { if( n < x.size() ) { x.resize(n); } return std::move(x); } // lvalue が渡された場合(引数を書き換えてはいけない) template< class T > T take( int n, T const& x ) { if( x.size() <= n ) { return x; } return T( x.begin(), std::next( x.begin(), n ) ); }
追記(3:05, 09/25): std::next
には実は刻み幅 n を渡せると知ったので, std::advance
から修正
このコードは,値渡しの場合と const
参照渡しの場合とで「より効率的なコード」が異なるため,
一律に値渡しのみ,または const
参照渡しのみ,とするより効率的になる場合が多い.
ただし,このコードは,実際には,
template< class T > std::vector<T> take( int n, std::vector<T> && x ) { if( n < x.size() ) { x.resize(n); } return std::move(x); } template< class T > std::vector<T> take( int n, std::vector<T> const& x ) { if( x.size() <= n ) { return x; } return { x.begin(), x.begin() + n }; }
のように,既知のクラスに対してのみ定義する方が,思わぬ事故を防げるケースが多い.
このような場合には, T&&
の推論で事故にあうケースは存在しないし,それ以外の間違いも ずっと起きにくい.
というわけで, Rvalue Reference を推論させるような状況は,実のところ,あまり存在しないのが実状である.*3