C++ で関数から object をどう返すか

VC++ における NRVO について別 entry に書いた -> http://d.hatena.ne.jp/janus_wel/20100301/1267397791

昨日同じような title で entry 書いたら ( http://d.hatena.ne.jp/janus_wel/20100221/1266766850 ) comment でいいことを教えてもらった ( http://d.hatena.ne.jp/janus_wel/20100221/1266766850#c ) ので調べてみたら最終的に今回の entry の title になった。 id:k1mid:tyru ありがとう。

RVO and NRVO

まず klm くんの教えてくれた RVO ( Return Value Optimization の略。余談だけど Return"ed" じゃないのは "Return" は「戻りの」という意味で形容詞として使っているからだね、仕方ないね ) から。

k1m 2010/02/22 05:31 RVO が効くことを信じて,中で non-static で作ってそのまま返すのが好きです。
http://en.wikipedia.org/wiki/Return_value_optimization

C++ で関数から文字列をどう返すか - KBDAHOLIC - やぬすさんとこ

klm たんが挙げた英語版 wikipedia の page に載ってる例はいわゆる NRVO ( Named Return Value Optimization ) と呼ばれるもので RVO の中でも賢い子の模様。普通 (?) の RVO は http://www.fides.dti.ne.jp/~oka-t/cpplab-retval-ctor.html で説明されているもの ( この page の一番最後の例が NRVO ) 。ちゃんと NRVO として日本語で言及されているのは http://d.hatena.ne.jp/Isoparametric/20091219/1261192152 の後半部分。

で、この最適化に関して C++ の規格書では「してもいいよ」という記述だけで「しなさい」ではない、ということらしいけど RVO なら大方の compiler はやってくれるらしい ( 伝聞 )。でまぁそこらへん確認しようと思ったんだけど C++98 の規格書を読むのもいまさらなので現在策定中の最新の draft を覗いてみる ( http://www.open-std.org/JTC1/sc22/wg21/ から pdf で paper が入手できる -> http://www.open-std.org/JTC1/sc22/wg21/docs/papers/2010/n3035.pdf ) 。すると 12.8 "Copying class objects" の 19 番目に以下のような記述があって

When certain criteria are met, an implementation is allowed to omit the copy construction of a class object, even if the copy constructor and/or destructor for the object have side effects.

俺訳) ある特定の基準に適合したとき、コピーコンストラクタかデストラクタもしくは両方がオブジェクトに対して副作用を持つ場合でも、実装はクラスオブジェクトのコピーコンストラクタの実行を省くことが出来る。

2010-02-16 Working Draft, Standard for Programming Language C++ 12.8.19

その基準、 criteria がこのあとにずらずら並んでいるんだけどそのうちのひとつに

in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object with the same cv-unqualified type as the function return type, the copy operation can be omitted by constructing the automatic object directly into the function’s return value

俺訳) ( 組み込み型でない ) クラス型を返す関数の return 文の中で、式が volatile なしの automatic な ( ストレージクラスが static または extern でない ) オブジェクトの名前であり、関数の返す型と同じ cv-unqualified ( const や volatile の制限のない ) な型であるとき、関数の戻り値の中に直接 automatic なオブジェクトを生成することでコピー処理は省略されうる。

2010-02-16 Working Draft, Standard for Programming Language C++ 12.8.19

ていうそれっぽいのがあって ( 正直英語の意味がわからないんだけど ) C++0x でも「してもいいよ」という表現であることがわかる。…てことは自分でしてくれる compiler をさがさにゃいかんということだよな。まぁこれは後述。

const 参照による一時オブジェクトの束縛

つぎ、 tyru さんが教えてくれた「 const 参照による一時オブジェクトの束縛」 ( 英語で言うと "binding temporary object with const reference" ? ) 。

tyru 2010/02/22 16:55 値渡しでもconst参照で受ければコピーは発生しませんよ。

C++ で関数から文字列をどう返すか - KBDAHOLIC - やぬすさんとこ

これは http://www.sun-inet.or.jp/~yaneurao/intensive/cppmaniax/chap0001.html に詳しく書いてあることかな。要は関数の引数に object を渡すときに void foo(const Bar& bar); とか書くけどこれの関数から戻ってくる版だね ( と俺は理解した )。以下のような code があるとき

class Foo;
Foo create_foo();
const Foo& foo = create_foo();

最後の行では以下のようなことが行われていると解釈するとつじつまがあう。

  1. create_foo() が評価される
  2. class Foo の instance が右辺値として返ってくる ( これは名前がない temporary object )
  3. 返ってきた object を foo という名前で参照できるようにする ( foo という別名をつける )

参照名が生きている間は参照先も生きているという rule と、 temporary object を参照するには const という前提をつけないと一律に処理できない ( その temporary object を生成する関数なりなんなりが定数を返すかもしれない ) という制限の 2 つが絡んでいる ( はず )。

codes

ここから実践。というわけで自分でも以下の source で実験してみた。

// main.cpp

#include <iostream>

#ifdef WIN32
#   define FUNCNAME __FUNCTION__
#else
#   define FUNCNAME __func__
#endif

using namespace std;

class Foo {
    public:
        Foo(void) { cout << FUNCNAME << "(void)" << endl; }
        Foo(const Foo&) {
            cout << FUNCNAME << "(const Foo&)" << endl;
        }
};

// return value optimization
Foo rvo(void) {
    return Foo();
}

// named return value optimization
Foo nrvo(void) {
    Foo foo;
    return foo;
}

int main(const int argc, const char* argv[]) {
    cout << "RVO" << endl;
    Foo foo1 = rvo();
    cout << endl;

    cout << "NRVO" << endl;
    Foo foo2 = nrvo();
    cout << endl;

    cout << "binding temporary object by const reference" << endl;
    const Foo& foo3 = rvo();
    cout << endl;

    cout << "binding named temporary object const reference" << endl;
    const Foo& foo4 = nrvo();
    cout << endl;

    return 0;
}

結果は以下のようになった。例によって VC++ 2008 Express Edition 、 MinGW g++ 4.4.0 、 andLinux g++ 4.3.3 の順番。 VC++ 2008 Express Edition の結果は最適化なしの場合。最適化ありの場合は http://d.hatena.ne.jp/janus_wel/20100301/1267397791 を参照。

RVO
Foo::Foo(void)

NRVO
Foo::Foo(void)
Foo::Foo(const Foo&)

binding temporary object by const reference
Foo::Foo(void)

binding named temporary object const reference
Foo::Foo(void)
Foo::Foo(const Foo&)
RVO
Foo::Foo(void)

NRVO
Foo::Foo(void)

binding temporary object by const reference
Foo::Foo(void)

binding named temporary object const reference
Foo::Foo(void)
RVO
Foo::Foo(void)

NRVO
Foo::Foo(void)

binding temporary object by const reference
Foo::Foo(void)

binding named temporary object const reference
Foo::Foo(void)

というわけで g++ は NRVO も support しているみたい。だけど RVO で済むならそっちを使った方が速いという結果もあるので ( http://d.hatena.ne.jp/Duke_mosso/20060929/p3 ) ここをテケトーにするのはよろしくない、と。

最後に結論。 RVO を期待して automatic object を返す ( 値返しをする ) 、というのが ( 関数を書く上では ) 何も考えなくて済むことがわかったので今後はそうしようと思う。 const 参照受けでもいいと言えばいいんだけどこの書き方混乱するひともいると思うのでできるだけ plain な書き方にする方向で。一般的にはどうか知らんけど、って一般的にも上記の code を使いたい compiler に食わして support しているか調べてから code を書けばいいだけだよな。