戻値とコンストラクタの妖しい関係

C++ では、関数の引数として、ユーザが定義したクラスのオブジェクトが 必要な場合は、たいてい、リファレンスを使う。 そうしないと、引数用に一時オブジェクトが生成され、 オブェクトのコピーが発生するからである。リファレンスを使えば、 一般的にはオブジェクトのアドレスが渡されるだけなので、オブジェクト 全体をコピーするよりはるかに効率的だ。

関数引数はリファレンスで渡すというのは、まあ、常識の部類であろう。 では、関数からの戻値としてオブジェクトを返す必要がある場合はどうだろうか。 オブジェクトが保持している属性値を返すようなときは、 const 修飾子を付けた上で、リファレンスで返すのが普通だろう。 その属性値を含むオブジェクト自身を呼び出し側が保持しているので、 アドレスだけ返しても特に問題はないからだ。

では、新しくオブジェクトを生成して返さなければならない場合はどうか。 こういうときに、初心者がよくやる間違いとして、 たいていの解説書に載っているのは次のようなコードである。

class Foo;

const Foo& createFoo()
{
    Foo aFoo;        // (A)

    // 何か処理して、

    return aFoo;     // (B)
}  

このコードがなぜダメかというと、まあ、解説は不要であろうと思うが、 簡単に言うと、(A) で作成されたオブジェクトは関数ローカルな存在なので、 (B) でそのリファレンスを返した直後に破壊されてしまうからである。 つまり、呼び出し側が次のようになっていたとして、

    const Foo& foo = createFoo();  

関数から返ってきたリファレンスを格納する foo は、すでに存在しないオブジェクトを 指していることになってしまっているというわけだ。

それじゃあ、というわけで、今度は次のようなコードを書いたりする。

class Foo;

const Foo *createFoo()
{
    Foo *ptrFoo = new Foo();    // (A)

    // 何か処理して、

    return ptrFoo;             // (B)
}  

このコード自体には、とくに問題はない。というか、いわゆる「ファクトリメソッド」 などと呼ばれるパターンにおいては、 関数の中で Foo の派生クラスのオブジェクトを生成することによって、 外部からその派生クラスの存在を隠蔽するための重要なテクニックになっていたりする。

「ファクトリメソッド」 については、また別の機会に書くことにするが、 一般的に言えば、 オブジェクトのポインタが返されたということは、呼び出し側は、 そのオブジェクトの後始末の責任も負わされたということであり、 ちょっとイヤな感じもする。

で、なぜ人々がこのようにリファレンスやらポインタやらを使って値を返したがるかというと、 値そのものを返す、というコーディングをすると、引数の場合と同様、 関数値から呼び出し側変数へのオブジェクトのコピーが発生するのではないか、 と恐れているからであろう。

ま、実際、その通りである。ちと長いが、以下のコードを見てほしい。

#include <iostream>
using namespace std;

class Foo {
public:
    // デフォルトコンストラクタ
    Foo() {
        cout << "Foo's default constructor" << endl;
    }

    // コピーコンストラクタ
    Foo( const Foo& foo ) {
        cout << "Foo's copy constructor" << endl;
    }

    // 代入オペレータ
    const Foo& operator = ( const Foo& foo ) {
        cout << "Foo's assignment operator" << endl;
    }
};

Foo foo()
{
    // ローカル変数
    Foo f;

    cout << "function foo" << endl;

    // 値を返す
    return f;
}

int main()
{
    // 変数を定義して、
    Foo f;

    // foo の戻値を代入する
    f = foo();

}  

これを手元の g++ でコンパイルして実行してみると、

    Foo's default constructor
    Foo's default constructor
    function foo
    Foo's copy constructor
    Foo's assignment operator

このように、コンストラクタが 3回、代入オペレータが1回、呼ばれている (青い太字の部分に対応する)。 オブジェクトのコピーも、"copy constructor" と "assignment operator" の、 都合 2回も起きている。

では、ここで、main の中での関数呼び出しを、次のように変えてみよう。

    // 変数の定義時に、関数の戻値を使う
    Foo f = foo();  

再コンパイルして実行してみると、

    Foo's default constructor
    function foo
    Foo's copy constructor

代入オペレータが呼び出されないのは当然としても、 コンストラクタの呼び出しも 2回に減っている。

さらに、今度は、関数 foo の return 文で、直接、コンストラクタを使って 値を返すようにしてみる。

Foo foo()
{
    cout << "function foo" << endl;

    return Foo();
}

実行。

function foo
Foo's default constructor

なんと、コンストラクタの呼び出しが、1回になってしまった !!

……白々しいですね。すみません。

これは、つまり、「戻値最適化」というやつで、いやしくも C++ コンパイラを名乗るからには、 必須の機能である。とはいっても仕組みは単純。

    Foo f = foo();  

というコードに遭遇したら、コンパイラは、

  1. 呼び出し側では f を格納する場所を確保 (確保するだけ、コンストラクタは呼ばない)
  2. そのアドレスを関数 foo に渡す
  3. 関数のほうでは、戻値用のオブジェクトのアドレスが渡ってきた場合 (かつ、それが NULL でない場合) は、 新たに一時変数を生成したりはせずに、 そのアドレスから始まる領域に対してコンストラクタを起動する

ということをやっているだけである。より賢いコンパイラだと、関数 foo

Foo foo()
{
    // ローカル変数 (A)
    Foo f;

    cout << "function foo" << endl;

    // 値を返す (B)
    return f;
}

という場合でも、(A) で定義された変数が、すべての return 文で 戻値として用いられている場合に限って、(A) で定義すべき変数として、 呼び出し側から渡ってきた戻値用のアドレスを使い回すことによって、 やはりコンストラクタの呼び出しを 1回で済ます、というワザも可能である。 『C++ オブジェクトモデル』という書物によると、このワザには、 「名前付き返却値最適化」などという名前がつけられているらしい *1

長々と書いてしまった (はたして、ここまで読んでくれた方は、何人いることやら)が、 私が言いたかったことは、ただ一つ。 「 オブジェクトそのものを値として返す関数を呼び出すなら、できるかぎり、 同時にコンストラクタで受け側の変数も定義せよ」ということである。

初出: 2001年4月9日

  1. S. B. リップマン著 『C++ オブジェクトモデル』 トッパン、p50〜54
    (うーむ、今は亡きトッパン…)

C++ ラビリンスの目次へ


トップページへ / Last modified: 2001-04-12 15:25:11 JST
Created by OKA Toshiyuki < oka-t@fides.dti.ne.jp >