const は消えていない

const が消える件… - 危ないRiSKのブログ


結論を書くと,別に const は消えていない. 単に Tint 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>

http://ideone.com/NnQKr


だから, template void f( U && x ); の内部で 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

http://ideone.com/n6oSD


この辺りの動作は, 本の虫: 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

*1:参照元の記事は,若干古い情報を元に書かれているが,大部分は今でも問題なく通じる. 未読の方は ぜひ読むべきである.

*2:なお,これは Rvalue Reference の semantics に相当する.

*3:そもそも,イテレータなり Boost.Range なり Pstade.Oven なりを使うほうが,上記のコードより効率的に書ける.