C++ 11 の rvalueリファレンスと一時オブジェクト
はじめに
この見えないけれどもそこにある一時オブジェクト(temporary object)というものは C++ においては実行効率や安全性などの面でとても重要なものなのですが、見えないので疎かにされがちです
しかし C++ 11 になり rvalue参照が追加されたことで一時オブジェクトについての理解がより重要になってきました
なぜかと言えば rvalue参照の rvalue というのは一時オブジェクトのことだからです
一時オブジェクトについては今までも何度か説明をしてきましたが、ここで C++ の一時オブジェクトについてもう一度おさらいしてみましょう
一時オブジェクトとは何者か
一時オブジェクト(temporary object)というのは、ソースコード中に明示的には書いていないけれどもそこにあるもののことです
名無しのオブジェクトと言ってもいいかもしれません
オブジェクトの型を暗黙に変換するとき、関数に引数を渡すとき、関数から戻り値を受け取るときなど式の途中に発生し、通常は完結式(full-expression)の終わりで消滅します
例えば次のような式に一時オブジェクトが隠れています
const std::string& s = std::string("foo") + "bar"; ↓ const std::string& s = std::string( operator+( std::string("foo"), "bar" )); void foo( const std::string& s ){ ... } foo( "foo" ); ↓ foo( std::string("foo"));
完結式
完結式(full-expression)というのは、他の式の部分式でない式のことです
1 General 1.9 Program execution 12 A full-expression is an expression that is not a subexpression of another expression. ...
if や while の条件式全体も完結式ですし for の各部も完結式ですが、大体セミコロン(;)までのことと思っておけば間違いありません
一時オブジェクトの寿命は基本的にはこの完結式の終わりまでですが、名前を与えられた一時オブジェクトだけは完結式の終わりを超えることができます
一時オブジェクトの名前
一時オブジェクトは名無しのオブジェクトであると言いました
その通りです
一時オブジェクトはプログラマが作るものではなくコンパイラが作るものなので、プログラマが識別するための名前を持っていません
ただ実体に名前が無いのはその通りなのですが、その実体を参照する参照変数を宣言することによって一時オブジェクトに名前を付けることができます
C++03 までの世界には一時オブジェクトを受け取れる参照は const参照しかありませんでしたが、
const int& foo = 10; const std::string& foo = "foo"; void foo( const std::string& s ){ ... } foo( "foo" );
C++ 11 の世界には rvalue参照があります
rvalue参照でも構いません
int&& foo = 10; std::string&& foo = "foo"; void foo( std::string&& s ){ ... } foo( "foo" );
auto&& でも構いません
auto&& foo = 10; auto&& foo = "foo";
const auto& でも構いません
const auto& foo = 10; const auto& foo = "foo";
手段はともかく名前を付けられた一時オブジェクトの寿命は延長されます
一時オブジェクトの寿命
一時オブジェクトの寿命はその一時オブジェクトが属している完結式の終わりまでです
12 Special member functions 12.2 Temporary objects 3. ... Temporary objects are destroyed as the last step in evaluating the full-expression (1.9) that (lexically) contains the point where they were created. ...
ただし、名前を付けられた一時オブジェクトの寿命はその名前が宣言されているスコープの終わりか元の完結式の終わりかどちらか長い方になります
// 完結式の終わりまで auto&& s = std::string("foo") + "bar"; // s のあるスコープの終わりまで auto&& s = "bar"; // foo を呼び出している完結式の終わりまで void foo( const std::string& s ){ ... } foo( "foo" );
さらにただし、関数の引数に渡された一時オブジェクトの寿命は延長されません
次の例では "foo" から作られる一時オブジェクトに付けられる名前は s です。bar ではありません
const std::string& foo( const std::string& s ){ return s; } const std::string& bar = foo( "foo" ); debug_printf( "%s\n", bar.c_str());
s の寿命は foo関数の中だけですね
これは元の一時オブジェクトの寿命である完結式の終わりまでより短いので、このような関数の引数に渡された一時オブジェクトの寿命は延長されないのです
寿命が延長されないということは bar に受け取ったと思っている一時オブジェクトの寿命は受け取った次の行では既に尽きています
そのためこの例を実行すると死にます
この例は一見したところでは一時オブジェクトに bar という名前を付けているかのように見えるかもしれませんが、先にも言った通りこの一時オブジェクトに付けられる名前は関数の引数の名前である s です
bar ではありません
bar は s を参照しているだけで s の寿命には何の影響も与えません
関数に渡した一時オブジェクトの寿命は延長されないということです
また別の例を見てみましょう
これは与えらえたレンジの逆順のレンジを返すという簡単な関数です
template<typename range_type> struct reverse_range { typedef std::reverse_iterator<decltype(std::begin(std::declval<range_type>()))> iterator_type; iterator_type first; iterator_type second; iterator_type begin() const { return first; } iterator_type end() const { return second; } reverse_range( range_type&& r ): first(std::rbegin(std::forward<range_type>(r))), second(std::rend(std::forward<range_type>(r))) { } }; template<typename range_type> inline reverse_range<range_type> make_reverse_range( range_type&& r ){ return reverse_range<range_type>( std::forward<range_type>( r )); }
一見問題なさそうですが、
int a[] = { 1, 2, 3 }; for( auto&& n : a ){ debug_printf( "%d\n", n ); } for( auto&& n : make_reverse_range( a )){ debug_printf( "%d\n", n ); } std::vector<int> v { 4, 5, 6 }; for( auto&& n : v ){ debug_printf( "%d\n", n ); } for( auto&& n : make_reverse_range( v )){ debug_printf( "%d\n", n ); } return 0;
一時オブジェクトを渡してしまうと死んでしまいます
一時オブジェクトの寿命は完結式の終わりまでなので for の中では既に無効なのです
for( auto&& n : make_reverse_range( std::vector<int> { 7, 8, 9 } )){ debug_printf( "%d\n", n ); }
関数の引数で受け取った const参照やその要素の参照を返してはいけないという決まりはこのためにあるものなのです
ところで上記の例は死にますが、次の例のように使う場合には問題ありません
debug_printf( "%s\n", foo( "foo" ).c_str());
この例でも関数に渡した一時オブジェクトの寿命は延長されませんが、この一時オブジェクトの寿命は元々この完結式の終わりまであるので完結式の中で使う分には何の問題もありません
一時オブジェクトと const参照
C++03 までの世界には一時オブジェクトを受け取れる参照は const参照しかありませんでした
そのため const参照が一時オブジェクトの寿命を延長すると説明されることもありましたが、しかし実は const参照であるかどうかというところは一時オブジェクトの寿命とは何の関係もありません
一時オブジェクトの寿命に関係あるのはそれが const参照されているかどうかということではなく、その一時オブジェクトに名前があるか無いかというところです
C++ 11 の世界では rvalue参照がありますから一時オブジェクトに名付けをするのに rvalue参照を使っても構いません
一時オブジェクトを受け取れる参照が今までは const参照しかなかったということからまた、一時オブジェクトは書き換え不可なオブジェクトであるという誤解が生まれてしまいました
しかし一時オブジェクトは書き換え不可なオブジェクトなどではありません
C++ 11 の世界には rvalue参照があります
rvalue参照は破壊用の参照ですからもちろん参照先を書き換えます
rvalue参照の参照先である rvalue というのは一時オブジェクトです
ですから一時オブジェクトは書き換え不可なオブジェクトなどではありません
一時オブジェクトの役割
一時オブジェクトの役割というのは、そのときそこにあるということです
ただそれだけです
見方を変えると一時オブジェクトはただそこにあるということだけが役割なので、存在しているその時点で既にその役割を終えていると言うこともできます
rvalue参照が追加された目的というのは、正に参照先がこの役割を終えた一時オブジェクトかどうかということを判別するためなのです
役割を終えた一時オブジェクトは後は消滅するだけなのですから、それが持っているものを使わずにただ捨ててしまうのは勿体ないではないかということなのです
rvalue参照について詳しくはこちらに譲ります
→ C++ 11 の rvalueリファレンス
一時オブジェクトの存在理由
一時オブジェクトの役割はそこにあることだと言いました
ではなぜそのような役割をするものが必要なのか、一時オブジェクトは何のために存在しているのか、なぜ一時オブジェクトというものが存在するのか、というとそれはプログラマがコードを書き易くするため、また読み易くするためです
例えば次のコードでは一時オブジェクトが使われています
std::string と const char* を operator+ でつなげて新しい std::string を作ることができます
std::string foo = "foo"; std::string foobar = foo + "bar";
これを一時オブジェクトを使わずに次のように書いても同じことですが、面倒ですね
std::string foo = "foo"; std::string foobar( foo ); foobar.append( "bar" );
これが一時オブジェクトの存在理由です
面倒でないように一時オブジェクトがあるのです
一時オブジェクトのコスト
一時オブジェクトがそこに生まれるかどうかは静的に決まる問題ですが、一時オブジェクトの生成と破棄は実行時に行われます
ということは実行時間も消費しますし、スタックも消費します。クラスの実装によってはヒープからメモリを確保するかもしれません
一時オブジェクトはただで手に入るものではありません
その一時オブジェクトが全体の実行に影響を与えるようなものでないのなら使っても構いませんが、実行時間の制約の厳しい箇所などでは使うべきではありません
一時オブジェクトはコードの書き易さや読み易さという静的な問題のために実行時にコストを払わなければいけないものだということに気を付けてください
実行時のコストを優先すべきなのか、生産性を優先すべきなのか、よく考えて使うようにしましょう
よく考えて使うようにしましょうというのは使い方もそうですが、書き方についても言っていますよ
何かコードを書いたときにそのコードから一時オブジェクトが生まれないかどうか、一時オブジェクトが生まれるならそれが許容できるかどうか、ということを考えましょうということです
一時オブジェクトはコード上は目に見えませんから、そこに暗黙の型変換が起きないかどうか、コードの挙動をよく把握しておく必要があります
一時オブジェクトと暗黙の型変換
これまでは一時オブジェクトの例には主に std::string を使ってきましたが、これは文字列から std::string への暗黙の型変換から一時オブジェクトが生じて都合がいいためでした
暗黙の型変換のあるところには一時オブジェクトが作られます
暗黙の型変換というのは変換先のクラスに変換元を引数にとるコンストラクタがあるか、または変換元のクラスに変換先へのキャスト演算子がある場合に可能になります
std::string と const char* の場合には std::string が const char* 1つを引数にとるコンストラクタを持っているために const char* から std::string への暗黙の型変換ができるようになっています
これは単純化すると下記のようなことになります
class cfoo { int m_foo; public: void foo() const { debug_printf( "cfoo::foo: %d\n", m_foo ); } cfoo( int foo_ ): m_foo(foo_) { debug_printf( "cfoo::cfoo( %d );\n", m_foo ); } ~cfoo(){ debug_printf( "cfoo::~cfoo\n" ); } cfoo( const cfoo& ) = default; cfoo& operator=( const cfoo& ) = default; }; void foo( const cfoo& foo ){ foo.foo(); } foo( 10 );
foo関数が期待しているのは cfoo クラスのインスタンスですが、渡されたのは 10 という整数です
このようにクラスのインスタンスが期待されたところに違うものが渡されたときは、その渡されたものをクラスのコンストラクタへの引数とみなして一時オブジェクトを作り、一時オブジェクトのインスタンスを代わりに渡そうというのが暗黙の型変換です
あるいはまたこのように変換先へ変換するキャスト演算子がある場合も暗黙の型変換により一時オブジェクトが作られます
class cbar { int m_bar; public: void bar() const { debug_printf( "cbar::bar: %d\n", m_bar ); } operator cfoo() const { return cfoo( m_bar ); } cbar( int bar_ ): m_bar(bar_) { debug_printf( "cbar::cbar( %d );\n", m_bar ); } ~cbar(){ debug_printf( "cbar::~cbar\n" ); } cbar( const cbar& ) = default; cbar& operator=( const cbar& ) = default; }; void bar( const cbar& bar ){ foo( bar ); } bar( 20 );
セマンティクスを維持した型変換という意味においては int の 1 と double の 1.0 との変換と同じです
ところで std::string と const char* が同じ文字列を意味しているというのは当たり前のことのように思えますが、上記の cfoo と int は同じものを当たり前に意味しているのでしょうか
これが当たり前である常に暗黙の型変換をしてくれた方がいいというのであればいいのですが、そうでなく、意図しないところで勝手に整数を cfoo にしてもらっては困るというのであれば、cfoo に int を一つだけとるコンストラクタを持たせてはいけません
C++ においては引数を一つだけ取るコンストラクタというものは暗黙の型変換のためにあるものであるということを覚えてください
暗黙の型変換を期待しないのであれば explicit を指定するようにしましょう
一時オブジェクトが生じるところをコンパイル時にエラーとして検出できるようになります
class cfoo { int m_foo; public: void foo() const { debug_printf( "cfoo::foo: %d\n", m_foo ); } explicit cfoo( int foo_ ): m_foo(foo_) { debug_printf( "cfoo::cfoo( %d );\n", m_foo ); } ~cfoo(){ debug_printf( "cfoo::~cfoo\n" ); } cfoo( const cfoo& ) = default; cfoo& operator=( const cfoo& ) = default; }; void foo( const cfoo& foo ){ foo.foo(); } foo( 10 ); // エラー