rvalue-reference で one-phase construction

前回の記事で説明したように、

auto p = std::make_shared<hoge>();

のような、「関数(あるいはコンストラクタ)の戻り値を変数に束縛する」コードは、*1 one-phase construction ではありません。
これはつまり、

struct person
  : private boost::noncopyable  // noncopyable なクラス
{
  std::string name;
  int age;
  
  person( std::string const& name_, int age_ )
    : name(name_), age(age_) {}
  person( std::string && name_, int age_ )
    : name( std::move(name_) ), age(age_) {}
  
};

auto p = person( "SubaruG", 25 ); // ダメ。コピー出来ない

このようなコードはコンパイルエラーになる、ということです。


まぁこのような場合は、代入形式の初期化を行わず、関数形式で

person p( "SubaruG", 25 );

と初期化すればいい話ではありますが、一方で、関数形式の初期化には ちょっとした問題があり、

int i( int() );

と書いた場合などに、多くの人が予想するような動作にならない( http://ideone.com/Jesh9 )ので、個人的にはあまり好きではないです。
それに何より、関数形式の初期化は、見た目があまり美しくないですし。*2
というわけで、代入形式の初期化で one-phase construction する方法があると嬉しいのですが、さて、なんとかならないでしょうか?


結論から言うと、 C++0x では、なんとかなります。 rvalue-reference を使えばいいのです。

auto && p = person( "SubaruG", 25 );  // おk。コピーは行われない。

rvalue-reference というと move semantics が有名ですが、実は C++0x の rvalue-reference の使い道は、それだけに留まりません。
このように one-phase construction を行う場合にも、 rvalue-reference は役立つ機能なのです。 rvalue-reference 万歳!


…と言うと、「ちょっと待って!」と思う人がいるかもしれません。


「このコード、安全なの? この p って、一時オブジェクト person( "SubaruG", 25 ) への参照を保持してるんでしょ? その一時オブジェクトって、 p を初期化する宣言文が終わったら、そこで破棄されるんじゃない? そうなると p って『既に破棄されたオブジェクト』への参照を保持することになるよ?」


という意見はもっともで、僕も最初にこのコードを見たとき*3は、そう思いました。


結論から言うと、このコードは安全です。
というのも、 C++ の規格では、このような「 rvalue-reference (と const lvalue-reference )の自動変数に直接束縛された一時オブジェクト」の寿命は、束縛先の自動変数の寿命に合わせて延長される*4と定められているからです。
もちろん、この規則は あくまで、純粋な一時オブジェクト( prvalue )を、参照型の自動変数に束縛する場合のみに適応されるもので、

auto && p1 = std::forward<person>( person( "SubaruG", 25 ) ); // これは person && 型なのでダメ
std::pair<int, person&&> p2( 0, person( "SubaruG", 25 ) );    // 自動変数じゃないのでダメ

上記のような「束縛対象が prvalue ではなく xvalue の場合」や「束縛先が自動変数ではなくメンバ変数の場合」には、この規則は適用されず、結果として既に破棄されたオブジェクトへの参照を保持する(しかもコンパイルエラーにならない!)ことになるので、個人的にはあまり使わないほうがいいと思いますが、
とにかく、

auto && p = person( "SubaruG", 25 );

のようなコードは安全で、 one-phase construction なのです。


で、それだけだと単に「へー、コピー出来ないオブジェクトでも、代入形式で初期化できるんだ。 でもそれ、関数形式の初期化を書き換えただけだよね?」と思うだけかもしれませんが、実はこの「 rvalue-reference によって一時オブエジェクトを束縛する」手法は、大きな可能性を秘めています。
というのも、このようにして rvalue-reference に束縛する一時オブジェクトは、別に直接コンストラクタによって生成されたオブジェクトでなくてもいいからです。
そう、例えば、関数によって返されたオブジェクトであっても、問題なく束縛することができます。


…と言うと、また「ちょっと待って!」と思う人がいるかもしれません。


「関数によって返されたオブジェクト、って言うけど、そもそも one-phase construction するメリットって、コピーもムーブも出来ないオブジェクトを扱えることでしょ? コピーもムーブも出来ないオブジェクトを、関数から返すことってできるの? そりゃ無理ってもんでしょ?」


その意見は、もっともです。 …いや、 C++98/03 の範囲では、もっともでした。
実は C++0x では、こういう「コピーもムーブも出来ないオブジェクト」であっても、 uniform initialization を使えば、関数から返すことができるようになったのです:

person make_charactor()
{
  return { "Chiffon Schroedinger", 14 };
}

これを行うには、「該当するコンストラクタが explicit 指定されていないこと」という条件こそありますが、
コピー出来る/出来ない、ということに関しては、特に条件はありません。
つまり、 C++0x では、コピー出来ないオブジェクトでも、問題なく関数の戻り値として使うことができるし、
戻されたオブジェクトを変数に束縛して使うことも、きちんと出来るのです。


結論: one-phase construction 万歳!


ただし、このように rvalue-reference によって一時オブジェクトを束縛する手法は、うっかりミスにより容易に dangling する可能性があり、しかもコンパイルエラーにならない、という危険性を持っているので、使う場合は自己責任でお願いします。


* * *


余談ですが、このような「あるクラスの値を返す関数」というのは、一種の「外部定義された名前付きコンストラクタ」と考えることができます。
つまり、 C++0x では、例えコピーもムーブも出来ないようなクラスであっても、関数定義によって外部から名前付きコンストラクタを導入できるようになったのです!
…と言いたかったけど、よく考えたら自動変数としてしか使えない(クラスメンバとして使いたい…。)ので、別にそんなことはなかったでござる

*1:この場合 hoge に関しては one-phase construction ですが、 p に関しては

*2: Uniform Initialization を使えばいいんじゃ? というツッコミは丁重にスルーさせていただきます

*3:本の虫: rvalue reference 完全解説

*4:規格: N3225 の 12.2 Temporary objects [class.temporary] 5