C++0x には rvalue reference という機能があります。
これは「一時オブジェクトへの参照」を扱うものであり、
struct X { std::string s; // 一時オブジェクトが渡された場合、 s_ は自由に変更できる // なので、 s_ の中身を「移動」して s を初期化する // これは s_ の中身をコピーするより効率がいい explicit X( std::string && s_ ) : s( std::move(s_) ) {} // 一時オブジェクト以外の為に const 参照版も用意 // こちらは普通にコピーする explicit X( std::string const& s_ ) : s( s_ ) {} };
このように使うことで、プログラムの効率を改善させることができます。
さて、そんな rvalue reference ですが、これはローカル変数に使うこともでき、
std::string && x = std::string("hoge");
このように書くと、 x は式 std::string("hoge") によって生成された一時オブジェクトへの参照を保持します。*1
これは
std::string && x = "hoge";
と書いても同じで、 x は "hoge" を std::string に暗黙変換して出来た一時オブジェクトへの参照を保持します。
なおこれは、いちいち rvalue reference を使わなくても、普通にローカル変数を使えばよく、
実際のところ、関数の引数に使う場合に比べると、使い道は殆ど無いのですが、
今後の説明の都合により、本記事ではこのようにローカル変数に rvalue reference を束縛させた例で解説していきます。
その場合でも基本的には、関数の引数に使うのと変わりません。
さて、そんな rvalue reference ですが、これはあくまで rvalue すなわち今後は使われない一時オブジェクトを保持するものなので、 lvalue すなわち今後も使う名前のついたオブジェクトを直接的に束縛させることが出来ません。
つまり、
std::string s = "hoge"; std::string && x = s; // エラー、 s は lvalue
このコードはコンパイルが通りません。また、
std::string && x = "hoge"; std::string && y = x; // エラー、 x も名前があるので lvalue
このコードもコンパイルが通りません。何故なら、 x の参照するオブジェクトは、(元々は一時オブジェクトだったとはいえ、今となっては)名前を持ち、今後も使う可能性があるからです。
もし名前のあるオブジェクトを rvalue reference に束縛させたい場合は、明示的にコピーするか、あるいは std::move を使って「今後はもう使わないので、一時オブジェクトとして扱っていい」という意図を示してやる必要があります:
std::string s = "hoge"; std::string && x = std::string(s); // おk。 x には s のコピーが束縛される std::string && y = std::move(s); // おk。 y には s そのものが束縛される
* * *
さて、前置きが長くなりました。
このように C++0x では基本的に、名前のあるオブジェクトすなわち lvalue を、直接的に rvalue reference に束縛できないようになっています。
しかし、
char const* str = "hoge"; std::string && x = str;
上記のようなコードでは、一見して
「名前のあるオブジェクトを rvalue reference に直接的に束縛している」
ように見えてしまいます。
実際には、上記のコードは
char const* str = "hoge"; std::string && x = std::string(str);
と書いているのと同じであり、 x は str を std::string に暗黙変換してできた一時オブジェクトを束縛しているので、問題はないのですが、
いくら問題ないとはいえ、
int i = 0; char c = 0; int && x = i; // コンパイルエラー。 lvalue は rvalue に束縛できない int && y = c; // おk。 char を int に暗黙変換した一時オブジェクトを束縛する struct Base {}; struct Derived : Base {}; Derived d = {}; Base && b = d; // これはコンパイルエラー、なぜなら Derived& -> Base& の型変換があるから
上のコードは、なんとなく「気持ち悪い」と感じてしまうのではないでしょうか。
一応、このような仕様になっているのにも理由があり、一番最初に挙げた struct X; の例で言うなら、
struct X { std::string s; explicit X( std::string && s_ ) : s( std::move(s_) ) {} explicit X( std::string const& s_ ) : s( s_ ) {} };
この X のコンストラクタに対して
char const* str = "hoge"; X x( str );
のように lvalue を渡した場合でも、 std::string && を取るコンストラクタが呼び出されて欲しいため、このような仕様になっているのです。
ですが、理由があるとはいえ、それでもやはりキモいものはキモいので、
この辺は何とかならないかなー、と思い、この記事を書きました。
解決策としては、( move constructor 以外で) rvalue reference に対して lvalue が渡された場合、
暗黙にコピーされた上で、その結果として作られた一時オブジェクトが rvalue reference に束縛される、ってのが良いと思います。
そうすれば、現行の C++0x では
// x の中身を大文字にする。 rvalue reference なので破壊的に書き換えていい。 std::string to_upper( std::string && str ) { for( char& c : str ) { // range based for c = std::toupper(c); } return std::move(str); } // lvalue が渡された場合に対する対応 std::string to_upper( std::string const& str ) { // コピーして上のコードに転送 return to_upper( std::string(str) ); }
って二つに分けて書くか、あるいは rvalue reference を使わずに(つまり効率を犠牲にして)
// x の中身を大文字にする。値渡しなので破壊的に書き換えていい。 // ただし rvalue が渡された場合は無駄にコピーされる // (この場合は実際には move されるとはいえ、一手間余分にかかるのは確かだし、 // std::string じゃない他のクラスの場合には、 move 非対応かもしれない) std::string to_upper( std::string str ) { for( char& c : str ) { c = std::toupper(c); } return std::move(str); }
って書かざるを得なかった部分を、
// lvalue を rvalue reference に束縛した際に暗黙にコピーされるなら、 // これだけあればいい。もちろん rvalue が渡された場合は無駄なコピーは無し std::string to_upper( std::string && str ) { for( char& c : str ) { c = std::toupper(c); } return std::move(str); }
のように、効率と使い勝手を両立させたコードにする事が可能になります。
問題は今までの C++0x コードとの互換性ですが、これは、
もし std::string const& を引数に取る関数が有った場合には、そちらを優先されるようにすれば、
今までのコードは破壊しないので、悪くない変更じゃないかなー、と思うのですが、どうでしょう。
いや、今月末に FDIS が発行される(であろう)現状じゃ、もはや変更することは無理でしょうけど、
こうなったらいいな、と思ったので、日記の記事にしてみた次第。
追記
lvalue を rvalue reference に束縛した際、暗黙にコピーされるようにすれば、
最初に挙げた struct X; の例も
struct X { std::string s; // これだけで済むなら、それに越したことはないよね explicit X( std::string && s_ ) : s( std::move(s_) ) {} };
のように簡潔に書くことが出来るようになります。
もっとも、その場合、
std::string s = "hoge";
X x(s);
と書いた場合には、本来であれば不要な std::string の一時オブジェクトが作られる*2ことになるわけですが、
実際の所、今でも、
char const* s = "hoge"; X x(s);
と書いた場合には、本来 不要な std::string の一時オブジェクトが作られる上、
std::string の move にかかる費用は実際問題として非常に安い事を考えると、極限まで効率にこだわらない限りは、大した差にはならないでしょう。*3
なお、もし極限まで効率に拘りたい場合には、コンストラクタをテンプレートにした上で SFINAE を使い、
struct X { std::string s; // s を与えられた引数から「直接構築」するようにする template< class T, // ただし、無差別に引数を取るのは色々と問題があるので、メタプログラミングにより // 与えられた型が std::string へと暗黙変換できる場合にのみ定義されるように class = typename std::enable_if< std::is_convertible<T, std::string>::value >::type > explicit X( T && t ) : s( std::forward<T>(t) ) {} // コンパイルエラーを分かりやすくするため、 std::string && を取るコンストラクタも用意 // std::string const& が渡された場合には、上のテンプレート版が呼ばれる explicit X( std::string && s_ ) : s( std::move(s_) ) {} };
と書けばよく、これは現行の C++0x でも普通に行える最適化ですが、
これにはメタプログラミングの知識がいりますし、
rvalue reference を導入した時ほど効率が改善されるようなこともないですし、
コンパイルエラーのメッセージも分かりにくくなる場合が殆どなので、オススメはしません。
現行ドラフトの C++0x においては、 X のコンストラクタは
struct X { std::string s; // 値渡しして move する explicit X( std::string s_ ) : s( std::move(s_) ) {} };
のように、値渡しして move するコンストラクタを一つ用意するのが、恐らく一番読みやすく、
かつ const 参照渡しのみを用意した場合に比べて効率も改善されているので、ベストかと思われます。