C++編【言語解説】 第17章 一時オブジェクト

先頭へ戻る

このエントリーをはてなブックマークに追加

この章の概要

この章の概要です。

一時オブジェクト

一時オブジェクトとは、ソースコード上には現れない、名前の無いオブジェクトのことです。 一時オブジェクトは、コンパイラの判断によって、自動的に生成・破棄するコードが埋め込まれます。

どんなときに一時オブジェクトが作られるのでしょうか。 代表的なのは、戻り値として、オブジェクトを返す場合です。 なお、ここでいう「オブジェクト」には、int型や double型といった組み込み型や、構造体型、クラス型などが含まれています。

#include <iostream>
#include <string>

std::string func()
{
	return "xyz";
}

int main()
{
	std::cout << func() << std::endl;
}

実行結果:

xyz

func関数の戻り値の型は、std::string なので、ポインタや参照ではなく、実体のあるオブジェクトです。 また、実際に返そうとしているのは、"xyz" という文字列リテラルです。 つまり、この場面において、名前の付いたオブジェクトは存在していません。

ここでコンパイラは、std::string型の一時オブジェクトを生成し、その初期値として "xyz" を与え、 この一時オブジェクトを関数の呼び出し元へ返すようなコードを生成します。 これは、次のように書いた場合と同じになります。

std::string func()
{
	return std::string("xyz");
}

名前を付けずに、「型名()」と書く構文があり、これで一時オブジェクトを明示的に作ることができます。 ( ) は、コンストラクタの呼び出しなので、引数付きのコンストラクタがあるのなら、中に実引数を書くことができます。

一時オブジェクトは、生成された場所を含んだ完全式の終わりのタイミングで破棄されることになっています。 完全式というのは、他の式の一部になっていない式のことを指します。
今回の例で言うと、一時オブジェクトが作られたのは、func関数を呼び出す式(関数呼び出し式)の中になりますが、 これは「std::cout 〜 std::endl」という式の一部です。 結局のところ、「std::cout 〜 std::endl」の実行が完了したタイミングが、一時オブジェクトが破棄されるタイミングとなります。


では、次のように書き換えたらどうなるでしょう?

#include <iostream>
#include <string>

std::string func()
{
	return "xyz";
}

int main()
{
	std::string s = func();
	std::cout << s << std::endl;
}

実行結果:

xyz

この場合も、一時オブジェクトは作られており、今回は完全式の終わりは「std::string s = func()」を終えたところということになります。 しかし今回の例では、一時オブジェクトが破棄されてしまう前に、s という変数へコピーしていますから、 一時オブジェクトが破棄されても、s へアクセスすることに問題はありません。


更に書き換えてみます。

#include <iostream>
#include <string>

std::string func()
{
	return "xyz";
}

int main()
{
	const char* s = func().c_str();
	std::cout << s << std::endl;
}

実行結果:




今度は、std::string::c_str関数(【標準ライブラリ】第2章)が返すポインタを受け取っています。[ std::string::c_str関数は、std::string が内部で持っている生の文字列のアドレスを返すため、 一時オブジェクトが破棄されてしまうと、一緒に不正なものになってしまいます。 この例では、一時オブジェクトは、「const char* s = func().c_str()」を終えたところで破棄されますから、 それより後で、変数s が保持しているアドレスを参照するのは不正です。

参照による束縛

先ほどの例の続きになります。 今度は、参照(前章)を使って書き換えてみます。

#include <iostream>
#include <string>

std::string func()
{
	return "xyz";
}

int main()
{
	const std::string& s = func();
	std::cout << s << std::endl;
}

実行結果:

xyz

今度は、一時オブジェクトを参照変数s で受け取っています。 今回、一時オブジェクトが破棄されるタイミングは、「const std::string& s = func()」の終わりです。
参照の場合、コピーを取っている訳ではないですから、一時オブジェクトが破棄された後で、 変数s を使うのは不正なように見えますが、実はこれは問題ありません

const を付けた参照変数で一時オブジェクトを受け取ると、その一時オブジェクトの寿命が、 その参照変数と同じ長さにまで延長されます。 この行為は、「一時オブジェクトの参照による束縛」などと呼ばれます。
今回の例で言えば、一時オブジェクトが破棄されるタイミングは、参照変数s が破棄される main関数の終わりまで延長されます。

なお、一時オブジェクトを受け取るには、const付きの参照でなければなりません。 非const参照ではコンパイルエラーになります。

ところが、VisualC++ 2013/2015/2017 では、上記のプログラムで、変数s を非const参照に変えてもエラーになりません。 Xcode ではコンパイルエラーになります。挙動としては、Xcode の方が正しいです。

ところで、func関数のように、オブジェクトの実体を返す場合、戻り値の型にも const を付けた方が安全です

const std::string func()
{
	return "xyz";
}

const を付けておくことによって、次のようなコードを適切にコンパイルエラーにすることができます。

func() = "abc";  // 戻り値の型が非const ならコンパイル可能 (しかし意味が無い)

これは、一時オブジェクトへの代入であり、すぐに消えてしまうので、普通は意味が無いことです。 意味が無いことはできないようにしておくことが望ましいと言えます。

また、1つ前のコラムに書いた、VisualC++ では非const参照で一時オブジェクトが受け取れてしまう問題も、 戻り値が const になっていれば防げます。 const付きの値から、非const参照への代入は const が外れてしまうので、const_cast を使わない限りは不可能です。

RVO (戻り値の最適化)

ここまでに取り上げてきた func関数では、戻り値が実体であることが少し気になるかも知れません。 前章では、引数の型をポインタや参照にすることで、関数呼び出しのコストを低減できるという話題がありましたが、 戻り値の場合は、こういった方法が取れません。 お分かりの通り、ローカル変数をポインタや参照で返してしまうと、関数を抜け出した時点で、その変数は消えてしまうため、 不正アクセスを起こしてしまいます。

次のように実装した場合を考えてみましょう。

#include <iostream>
#include <string>

const std::string func()
{
	std::string s = "xyz";
	return s;
}

int main()
{
	const std::string t = func();
	std::cout << t << std::endl;
}

実行結果:

xyz

この場合、func関数内で s を生成するためにコンストラクタが、 呼び出し元の t を生成するためにコピーコンストラクタが呼び出されます。 また、2つのオブジェクト s, t が生成されたので、デストラクタも2回呼び出されます。

では次のように実装した場合はどうでしょう?

#include <iostream>
#include <string>

const std::string func()
{
	return std::string("xyz");
}

int main()
{
	const std::string t = func();
	std::cout << t << std::endl;
}

実行結果:

xyz

一見すると、一時オブジェクトを生成するためのコンストラクタ、 t を生成するためのコピーコンストラクタ、そして2つのデストラクタが呼び出され、 結局先ほどの例と変わらないように見えます。
しかし実際には、ほとんどのコンパイラでは、次のように書いたのと同等なコードが生成されます。

int main()
{
	const std::string t = std::string("xyz");
	std::cout << t << std::endl;
}

つまり、コンストラクタ1回とデストラクタ1回だけが呼び出されます。

これは、RVO (戻り値の最適化) という最適化です。 この最適化によって、戻り値を返すための一時オブジェクトを生成しないように、 受け取り側のメモリ領域へ直接オブジェクトを生成する形に置き換えられます。
RVO は非常に一般的な最適化手法であり、この最適化はほぼ確実に行われると期待できます。

lvalue と rvalue

左辺値右辺値という用語があります。 これまた結構理解しづらい概念ですが、参照や一時オブジェクトの話題になると、よく登場するものなので、 ここで説明しておきます。

まず、左辺値と右辺値は、C言語にも存在しています。 C言語においては、言葉のイメージ通り、代入演算子の左側が左辺値、右側が右辺値と考えて差し支えありません。 しかし、C++ ではこんなに単純ではありません。

C++ における左辺値は、(例外もありますが基本的には)名前が付いているものです。 右辺値はその逆で、名前が無いもののことです。 つまり、この章で取り上げてきた一時オブジェクトは右辺値です。
このように、代入演算子の左側とか右側とかは何ら関係が無いので、 C++ の場合は、左辺値を lvalue、右辺値を rvalue と表記することが多いです。 当サイトでも、今後は lvalue、rvalue と表記します。

幾つか例を挙げてみます。

int func();

int main()
{
	int a, b;
	
	a = 0;       // a は lvalue、0 は rvalue
	b = a;       // b は lvalue、a も lvalue
	
	func();      // func() が返すのは一時オブジェクトなので rvalue
	a = func();  // a は lvalue、func() は rvalue
}

変数a が、代入演算子の左側に登場しても右側に登場しても、a という名前があるので常に lvalue です。 func() が実体を返す関数なので、その戻り値は一時オブジェクトになので名前がありません。 そのため、func の呼び出し式は rvalue であることを確認して下さい。

rvalue へ代入することはできません。 次の例を見れば、確かにできそうにないことが分かると思います。

int func();

int main()
{
	int a = 10;

	0 = a;       // 0 は rvalue なのでエラー
	3 + 5 = a;   // 3 + 5 は rvalue なのでエラー
	func() = a;  // func() は rvalue なのでエラー
}

lvalue への代入については、const の有無によって、代入できるかできないかが決まります。 これは特に難しくなく、以下の例の通りです。

int main()
{
	int a;
	const int b = 10;

	a = 10;  // a は const無しの lvalue なので OK
	b = 10;  // b は const付きの lvalue なのでエラー
}

これまでに取り上げてきた参照(リファレンス)は、lvalue reference(左辺値参照)と呼ばれることもあります。 言葉通り、lvalue を参照するものであるということです。 参照が、何も参照していない状態を作ることができず、 常に何らかの変数を参照していなければならないというルールとも合致することが分かると思います。
なお、lvalue reference自身は、参照変数として名前を持っているので lvalue です。

int func();
int& func2();
const int& func3();

int main()
{
	func();   // 戻り値は一時オブジェクトなので rvalue
	func2();  // lvalue reference を返しているので lvalue
	func3();  // lvalue reference を返しているので lvalue
	
	int a = 10;
	int& b = a;  // b は lvalue、a も lvalue
}

しかし、lvalue reference といいつつ、const付きの場合に限っては rvalue を参照できます。 これは本章において、一時オブジェクトを const参照で束縛する例で確認しました。 const の無い lvalue reference では、rvalue を参照できないため、一時オブジェクトを束縛することもできないという訳です。


コンパイラが出力するエラーメッセージには、lvalue や rvalue という言葉が含まれていることがあります。 この項で得た知識があれば、こういったエラーメッセージも理解しやすくなるでしょう。

C++11 (rvalue reference、右辺値参照)

C++11

lvalue を参照する従来の参照に加えて、C++11 には rvalue を参照する rvalue reference (右辺値参照) が追加されています。 rvalue reference は、「型名&&」と表現します。

int func();

int main()
{
	int a = 10;
	int&& b = 10;      // 10 は rvalue なので OK
	int&& c = a;       // a は lvalue なのでエラー
	int&& d = b;       // b は lvalue なのでエラー
	int&& e = func();  // func() は rvalue なので OK
}

rvalue reference 自身は lvalue であることも確認して下さい。 また、rvalue を参照できるということは、一時オブジェクトを参照できるということです。

lvalue を代入することはできませんが、実はキャストすれば代入可能です。

int main()
{
	int a = 10;
	int&& c = static_cast<int&&>(a);       // OK
}

ただ、キャストするよりも、キャストをラップした std::move関数を使った方が、 意図が明確ですし、何より目に見えるキャストを避けられるので良いです。 なお、この関数は、utility という名前の標準ヘッダに定義されています。

#include <utility>

int main()
{
	int a = 10;
	int&& c = std::move(a);       // OK
}

このように rvalue reference は、一時オブジェクトを参照するために使えますが、それだけなら const付きの lvalue reference でも可能です。 rvalue reference の存在意義にはもう1つ、ムーブセマンティクスを実現するというものがあります。 これについては、第27章で改めて取り上げます。

なお、rvalue reference は、VisualC++、Xcode のいずれでも使用できます

C++11 (参照修飾子)

C++11

C++11 では、メンバ関数に対して、参照修飾子(リファレンス修飾子)を指定できるようになっており、 thisポインタが指すオブジェクトが lvalue か rvalue かによって、呼び出されるメンバ関数を変えることができます。

#include <iostream>

class MyClass {
public:
	void func() & {
		std::cout << "lvalue" << std::endl;
	}

	void func() && {
		std::cout << "rvalue" << std::endl;
	}
};

MyClass getMyClass()
{
	return MyClass();
}

int main()
{
	MyClass a;

	a.func();
	MyClass().func();
	getMyClass().func();
}

実行結果:

lvalue
rvalue
rvalue

メンバ関数の宣言の際、& を後ろにつけると、呼出し時のオブジェクトが lvalue である場合に呼び出されることになります。 && を付けた場合は、rvalue の場合に呼び出されます。

この例のように、& の付いたメンバ関数と、&& の付いたメンバ関数は、オーバーロードできます。 ただし、これらの修飾が無い、通常のメンバ関数との間ではオーバーロードできません

この機能は、VisualC++ 2013/2015 では対応していませんが、2017 は対応しています。 Xcode では使用できます。


練習問題

問題@ ある関数の仮引数が次のようになっているとき、実引数が渡される際に何が行われているか説明して下さい。

void func(std::string s);
void func2(const std::string& s);

問題A 次のプログラムはコンパイルエラーになります。理由を説明して下さい。

#include <string>

void func(std::string& s) {}

int main()
{
	func("abc");
}


解答ページはこちら

参考リンク

更新履歴

'2017/7/30 clang 3.7 (Xcode 7.3) を、Xcode 8.3.3 に置き換え。

'2017/3/25 VisualC++ 2017 に対応。

'2016/10/15 clang の対応バージョンを 3.7 に更新。

'2015/10/12 clang の対応バージョンを 3.4 に更新。

'2015/9/5 VisualC++ 2012 の対応終了。

'2015/8/18 VisualC++2010 の対応終了。

'2015/8/15 VisualC++ 2015 に対応。

'2014/10/18 clang 3.2 に対応。

'2014/8/23 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ