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

先頭へ戻る

この章と同じ(または似た)情報を扱うページが、Modern C++編 (C++11/14/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参照ではコンパイルエラーになります。

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

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

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

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

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

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

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

RVO (戻り値の最適化)

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

実際には、実体のまま戻り値を返しても、効率的なコードが生成される可能性が高いです。何が起きるのでしょうか?

次のプログラムを使って、オブジェクトの生成や破棄がどれだけ行われるのかを調べてみます。

#include <iostream>

class C {
public:
    C()         { std::cout << "constructor" << std::endl; }
    C(const C&) { std::cout << "copy constructor" << std::endl; }
    ~C()        { std::cout << "destructor" << std::endl; }
};

C func()
{
    return C();
}

int main()
{
    C c = func();
}

素直に処理を追いかけて考えると、以下の4つの呼び出しが起こるように思えます。

  1. func関数の戻り値にする一時オブジェクト C() を生成するコンストラクタ
  2. 戻り値を使って、c を生成するコピーコンストラクタ
  3. 戻り値を破棄するデストラクタ
  4. s を破棄するデストラクタ

しかし実際に試すと、以下の結果を得られるでしょう。

実行結果:

constructor
destructor

つまり、コピーコンストラクタの呼び出しが省かれ、その分、デストラクタも1回分減っているということです。省略されたコピーコンストラクタとは、一時オブジェクトを生成する際のもので、これを省略した代わりに、戻り値を利用する呼び出し側の方へ直接、オブジェクトを生成します。こうして、余分なコピーを省き、効率を向上します。

これは、コンパイラが行う最適化で、RVO (Return Value Optimization: 戻り値の最適化) と呼ばれている手法です。この最適化がすべてのコンパイラで行われる保証はありませんが、まず間違いなく行われるといえるほど一般的なものです。

VisualStudio での Debugビルド時のように、この最適化が起こらないことはあり得ます。

この最適化で呼び出されなくなったコピーコンストラクタとデストラクタには、標準出力への出力という処理が含まれていることにも注目して下さい。つまり、何か副作用を持った処理が含まれていたとしても、容赦なく省略しています。普通、コンパイラが行う最適化は、意味が変わらないように行われるものですから、これは非常に特殊です。

さて、プログラムを次のように変形すると、結果が変わるかも知れません。

#include <iostream>

class C {
public:
    C()         { std::cout << "constructor" << std::endl; }
    C(const C&) { std::cout << "copy constructor" << std::endl; }
    ~C()        { std::cout << "destructor" << std::endl; }
};

C func()
{
    C c;
    return c;
}

int main()
{
    C c = func();
}

VisualStudio 2015 の Debugビルドで試すと、次の結果を得られます。

実行結果:

constructor
copy constructor
destructor
destructor

何も最適化されていないようです。

前のサンプルプログラムでは一時オブジェクトを返していましたが、今回は名前のあるオブジェクトである点に違いがあります。とはいえ実際のところ、このサンプルプログラムの形であっても、前のサンプルプログラムと同じ理屈で最適化を行うコンパイラが多くあります。例えば、clang は最適化を行って、次の実行結果を出力します。

実行結果:

constructor
destructor

最適化の理屈は同じで、コピーを省略し、その結果、デストラクタの呼び出しも減ります。前の最適化と同じ結果になるので、これも RVO と呼ぶことがありますし、一時オブジェクトではなく名前を持ったオブジェクトであることを強調して、NRVO (Named Return Value Optimization: 名前付き戻り値の最適化) と呼び分けることもあります。

NRVO を働かせるためには、ローカルオブジェクトの型が関数の戻り値型と同じであることと、return文に与える式が、そのローカルオブジェクトの名前だけであることを守らなければなりません。ただし、関数の仮引数を return する場合には NRVO は働かないのが普通です。


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

(→Modern C++編

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つ、ムーブセマンティクスを実現するというものがあります。

C++11 (参照修飾子)

C++11

(→Modern C++編

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 の場合に呼び出されます。

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

この機能は、VisualStudio 2015 では対応していません。


練習問題

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

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

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

#include <string>

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

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


解答ページはこちら

参考リンク



更新履歴

'2018/7/17 「RVO (戻り値の最適化)」の項を全面的に書き直した。

'2018/4/5 VisualStudio 2013 の対応終了。

'2018/4/2 「VisualC++」という表現を「VisualStudio」に統一。

'2018/1/5 コンパイラの対応状況について、対応している場合は明記しない方針にした。
Xcode 8.3.3 を clang 5.0.0 に置き換え。

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

≪更に古い更新履歴を展開する≫



前の章へ(第16章 コピー操作と参照)

次の章へ(第18章 const の活用)

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

Programming Place Plus のトップページへ


このエントリーをはてなブックマークに追加
rss1.0 取得ボタン RSS