2006年01月31日

戻り値とリファレンス

戻り値とリファレンス

戻り値にリファレンスを用いる場合には、引数にリファレンスを用いる場合以上に注意が必要です。例えば、以下のようにうっかりローカル変数のリファレンスを返してしまうと、戻り値は関数呼び出しが終わった瞬間に不正な値となってしまいます。

001 struct test_obj
002 {
003 test_obj() { cout << "test_obj: construction" << endl; }
004 ~test_obj() { cout << "test_obj: destruction" << endl; }
005 int val_;
006 };
007
008 test_obj& f1()
009 {
010 test_obj obj;
011 obj.val_ = 1;
012 return obj; // おっと、ローカル変数を返してる。
013 }
014
015 void f()
016 {
017 test_obj& obj = f1(); // f1 の戻り値は関数の終わりで無効になっている。
018 cout << obj.val_ << endl; // もし動いても、それは偶然。
019 obj.val_ = 2; // これも危険。
020 cout << obj.val_ << endl; // やっぱり偶然。
021 }

メンバ変数を返すようにしていれば安心かというと、そうでもありません。例えば const なメンバ関数が const でないリファレンスメンバを返すと、以下のように奇妙な現象を引き起こします。(参照: Effective C++ 29項 内部データの「ハンドル」を返すのはやめよう)

001 class A {
002 int i_; // データ
003 int& r_; // そのリファレンス
004 public:
005 A() : i_(0), r_(i_) {}
006 int GetVal() const { return i_; } // データを返す
007 int& GetRef() const { return r_; } // あっ!
008 };
009
010 void f()
011 {
012 const A a; // a は const
013 cout << a.GetVal() << endl; // データは初期値
014 a.GetRef() = 1; // GetRef は const メンバ関数なので、この呼び出しはOK.
015 cout << a.GetVal() << endl; // const な a のデータが書き換わってしまう!
016 }

そもそも、リファレンスを返すインタフェースは実装に制限を加えてしまうという意見もあります。例えば、以下のように値を返すインタフェースであれば、その実装でメンバを返そうがテンポラリを返そうが問題ありません。

001 class Base {
002 public:
003 virtual ~Base() {}
004 virtual int GetVal() const = 0; // 値を返すインタフェースなら…
005 };
006
007 class A : public Base {
008 public:
009 virtual int GetVal() const { return 0; } // 定数でもOK.
010 };
011
012 class B : public Base {
013 public:
014 virtual int GetVal() const { return int(1); } // テンポラリもOK.
015 };
016
017 class C : public Base {
018 int i_;
019 public:
020 C() : i_(2) {}
021 virtual int GetVal() const { return i_; } // メンバ変数だってOK.
022 };

しかし、以下のようにリファレンスを返すインタフェースだったならば、その実装でテンポラリを返すことが出来なくなってしまいます。たとえ戻り値が const リファレンスであったとしても、「const なリファレンスの初期化によるテンポラリオブジェクトの延命」で述べたようにテンポラリオブジェクトの寿命が切れてしまうため、結果は悲惨なものとなってしまうでしょう。

001 class Base {
002 public:
003 virtual ~Base() {}
004 virtual const int& GetVal() const = 0; // リファレンスを返すインタフェースだと…
005 };
006
007 class A : public Base {
008 public:
009 virtual const int& GetVal() const { return 0; } // error.
010 };
011
012 class B : public Base {
013 public:
014 virtual const int& GetVal() const { return int(1); } // error.
015 };
016
017 class C : public Base {
018 int i_;
019 public:
020 C() : i_(2) {}
021 virtual const int& GetVal() const { return i_; } // メンバ変数ならOK.
022 };

確かにリファレンスを返せば効率は改善されるかも知れません。しかし、戻り値に限っていえば、不要なテンポラリオブジェクトを取り除くような最適化が規格によって許されています。この「戻り値最適化」に関しては「More Effective C++ 項目20: 戻り値最適化の促進」を参照していただくとして、まずは運命をコンパイラに委ね、値を返すような関数を基本としておきましょう。大抵の場合、効率が問題になって初めて戻り値をリファレンスにすべきかどうかで悩んでも遅くはないはずです。

これまで、効率の面だけに着目してきましたが、クラスの派生が絡んでくると状況は一変します。インタフェースを返す関数が値を返してしまうと「スライシング問題」を引き起こしてしまうため、そのような関数はポリモーフィズムが有効なリファレンスかポインタを返さなければなりません。

001 class Base {
002 public:
003 virtual ~Base() {}
004 virtual void Name() const { cout << "Base" << endl; }
005 };
006
007 class Sub : public Base { // Base から派生
008 public:
009 virtual void Name() const { cout << "Sub" << endl; }
010 };
011
012 Base GetSub1() { return Sub(); } // マズイ! Sub を返しているつもり。
013 const Base& GetSub2() { static Sub sub; return sub; } // これならOK.
014
015 void f() {
016 GetSub1().Name(); // Base と出力される。
017 GetSub2().Name(); // Sub と出力される。
018 }

つまり、関数の戻り値を「値渡し」にするか「参照渡し」にするかは、効率で決めるのではなく設計の一部ということです。チューニングの必要に迫られるまで、設計を歪めるほどの最適化は必要ありません
posted by シンビアン at 08:33| Comment(0) | TrackBack(0) | Symbian OS C++ 実践開発技法 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック
×

この広告は1年以上新しい記事の投稿がないブログに表示されております。