読者です 読者をやめる 読者になる 読者になる

Universal Reference を知るべき複数の理由

復習第2弾。
universal reference って名前は知らなくていいので、C++のこれらの挙動だけは知っててほしいです。
前の記事と合わせてどうぞ。

Universal Reference は以下 URefs と略させて頂きます。

URefs とオーバーロード

class some_class
{
public:
    template <typename T>
    void f( T const& ) { std::cout << "called T const&" << std::endl; }
    template <typename T>
    void f( T&& )      { std::cout << "called T&&" << std::endl; }
};

int main()
{
    some_class sc;
    sc.f( 0 );
    int x = 0;
    sc.f( x );
}

このコードの結果は、

called T&&
called T const&

ではありません
正しくはこう

called T&&
called T&&

もぅ C++ マジ闇。。。リスカしょ。。。

どうしてこうなった

先日(というか同日)の記事を読んだ人はわかると思いますが、type deduction が発生する場合、template <typename T> some_class::f(T&&) の "T&&" は URefs、すなわち lvalue にも rvalue にもマッチする関数となります。
つまり、さっきの some_class::f の正しい解釈は、

  • const lvalue で呼び出された場合、 T const& の f が呼ばれる
  • lvalue(not const) または rvalue で呼び出された場合、T&& の f が呼ばれる

となります。
これは、

class some_class
{
public:
    void f( const another_class& ); // 1
    void f( another_class&& );      // 2
};
another_class make_another_class() { ... }

some_class sc;
another_class ac;
sc.f( ac );                   // 1 が呼ばれる
sc.f( make_another_class() ); // 2 が呼ばれる

のように、URefs で無い場合と混同してしまうことが非常に多いですので、気をつけましょう。そうでないとプロジェクトが謎のバグで炎上します。
(というかこれ知らなかったら絶対間違えるでしょ…

URefs はとりあえずなんでも食う

std::string search_name( int id ) { ... }

class person
{
public:
    template <typename T>
    person( T&& t ) : name( std::forward<T>( t ) ) {}
    person( int id ) : name( search_name( id ) ) {}
private:
    std::string name;
};

std::size_t my_id = 100;
person p( my_id );      // error: invalid conversion 
                        //        from ‘unsigned int’ to ‘const char*’
どうしてこうなった

タイトルに有る通り、 URefs は食いしん坊さんなのでどんな型でもとりあえずマッチします。
今回の例では、 my_id の型が unsigned int です。なので、person(int) よりも person(unsigned int&) になれる URefs のほうが呼ばれてしまいます。型気をつければいいんですけど、人間間違えるときもあるので気をつけましょう。

URefs はやっぱりなんでも食う

class some_class
{
public:
    template <typename T>
    some_class( T&& ) { ... }            // URefs ctor
    template <typename T>
    some_class& operator=( T&& ) { ... } // URefs copy
};

このようにクラスを書いたとします。
template が使われたコンストラクタ、コピー代入演算子は暗黙的に生成されるデフォルトコンストラクタとコピー(ryを抑制しませんので、結局のところ

class some_class
{
public:
    template <typename T>
    some_class( T&& ) { ... }                  // URefs ctor
    some_class( some_class const& ) = default; // default copy ctor
    some_class( some_class&& ) = default;      // default move ctor
    template <typename T>
    some_class& operator=( T&& ) { ... }                  // URefs copy
    some_class& operator=( some_class const& ) = default; // default copy copy
    some_class& operator=( some_class&& ) = default;      // default move copy

となります。
これがどんな悲劇を生むのか、まずはそのコードを見ましょう。

some_class sc;
const some_class csc;

some_class sc1( sc );               // 1
some_class sc2( csc );              // 2
some_class sc3( std::move( sc ) );  // 3
some_class sc4( std::move( csc ) ); // 4

「え?これ template の部分関係なくね?」と思った人がいるかもしれません。
では、1~4は実際どれを呼んでるのでしょうか。
答えはこれです!

  1. URefs ctor
  2. default copy ctor
  3. default move ctor
  4. URefs ctor

理由は最初の URefs とオーバーロードに書いてますが、気づかないとやばいです。
謎のバグで残業代も出ないのに会社に泊まり込みになるかもしれません。気をつけましょう。

URefs と initializer_list

template parameter の type deduction が std::initializer_list のときうまく働かない(auto はちゃんとうごく)問題。規格(§14.5.6.2)を見る限りなるほど、って感じですがややこしいのです。stackoverflow にあるので、気になる方は読んでみてください。

stackoverflow -Universal references and std::initializer_list-

どう立ち向かうか

「URefs 使うときはオーバーロードはやめよう」