Gobble up pudding

プログラミングの記事がメインのブログです。

MENU

C++でオブジェクトを返す関数を書いてもいいのか問題

スポンサードリンク


C++でオブジェクトを返す関数を書いてもいいのか問題→OKです。
遅かったらプロファイル取りましょうで終わりです。

かなり過去にC++でstd::stringをどう返すべきかという記事を書いたこともあり結局いま、例えば、関数内でvectorを加工してその結果を呼び出し側で使いたいニーズがある場合のコードはどう書くのが正解なの? ということを思い至り備忘録として残しておきます。
※話が複雑になるので、例えばsortするときのようにあらかじめあるvectorを加工する場合ではないことを明記しておきます

主にC++11が出る前のように

void foo(std::vector<int>& v) {
  ...(vector書き換え)
}
std::vector<int> vec = { 1, 2, 3 };
foo(vec);

とやるのがいいのか、

std::vector<int> foo() {
  std::vector<int> v = { 1, 2, 3 };
  ...(vector書き換え)
  return v;    // ここが気になる!
}
std::vector<int> vec = foo();

のがいいのかという話です。自信がなかったので調べてみました。

前者だと前時代的な感じだし、引数経由で結果を受け取るっていうのもアレだし、
参照だとそもそも呼び出し側で書き換えてますよってのがよくわかんない…
っていうのもあって微妙感ありますよね。
後者で書ければいいけど、可読性重視したけどコピーが発生して速度低下したら元も子もない。
さらに、後者の場合でstd::moveの必要があるのかどうなのか?ってあたりも気になりました。

結論

ググったらすぐわかることなのですが、 こことか ここに答えが書いてあります。

もはや現代では(あらかじめ何か値があるものを外から受け取って加工するわけでないこのケースでは)オブジェクトを返す関数を書くのが最適解という結論が出てしまいました。

検証コード

www.bit-hive.com
のコードを参考にRVOが効いているかどうか検証しました。

#include <iostream>
#include <vector>

// xにrbpレジスタの値を設定(gcc)
#define CURRENT_RBP(x) \
  __asm__("mov %%rbp, %0;" \
            :"=r"(x) \
          );

// RVOが適用されているかをチェックするマクロ
#define CHECK_RVO(tag, object) \
{ \
  unsigned long rbp; \
  CURRENT_RBP(rbp); \
  unsigned long address = reinterpret_cast<unsigned long>(&object); \
  std::cout << "rbp:     0x" << std::hex << rbp << std::endl; \
  std::cout << "address: 0x" << std::hex << address << std::endl; \
  if (rbp > address) { \
    std::cout << tag << ": RVO isn't applied." << std::endl; \
  } else { \
    std::cout << tag << ": RVO applied." << std::endl; \
  } \
}

class Something {
public:
  int value;
  Something() : value(0) {}
  Something(const Something& obj): value(obj.value) {
    std::cout << "Copy constructor called." << std::endl;
  }
  Something(const Something&& obj) noexcept: value(std::move(obj.value)) {
    std::cout << "Move Constructor called." << std::endl;
  }
};

// 名前付きのオブジェクトを返す関数
Something create_object() {
  Something object;
  CHECK_RVO("object", object);
  object.value = 1;
  return object;
}

// 名前付きのオブジェクトを返す関数 vector
std::vector<Something> create_objects() {
  std::vector<Something> objects;
  //std::vector<Something> objects {Something(), Something(), Something()}; だとコピーになる
  CHECK_RVO("objects", objects); 
  objects.emplace_back(Something());
  objects.emplace_back(Something());
  objects.emplace_back(Something());
  int i = 0;
  for (Something& s : objects) {
    s.value = ++i;
  }
  return objects;
}

int main() {
  Something object1 = create_object();
  std::cout << "Check result" << std::endl;
  std::cout << object1.value << std::endl;
  
  std::cout << "------" << std::endl;
  std::vector<Something> objects = create_objects();
  std::cout << "Check result" << std::endl;
  for (Something& s : objects) {
    std::cout << s.value << std::endl;
  }
}

実行結果

https://wandbox.org/permlink/3SHbmZ1DybX6nXB4

そもそもRVOってどうやってやってるんだという話

上記の検証コードを見ていただけるとわかりますが、
要は関数の戻り先の領域ににあらかじめ値を格納してやれば、
関数が終わってもその値がコピーせずに使える…というものらしいです。

上記のマクロ(CHECK_RVO)でチェックしているところはこんな感じです。
RBP(64bitスタックベースポインタレジスタ)は要は関数の始まりのアドレスです。
スタックはあとのが上に積まれるので(ただし、アドレスは小さい方向に成長していく)、
通常は、

+---------------------------+
|0x7fff1e32bc60 オブジェクト |
+---------------------------+
|0x7fff1e32bc4c 関数 RBP    |
+---------------------------+

となっているのだけれど、

+---------------------------+
|0x7fff1e32bc60 関数 RBP    |
+---------------------------+
|0x7fff1e32bc9c オブジェクト |
+---------------------------+

RVOが効いたときは、このようになっています。
これでコピーしなくてよい!めでたしめでたしとなるわけです。

自然なコードを書いてもよさそう。

例外的なケース

とはいえ、sortのときのように何かを渡して加工する場合を考えると…

std::vector<int> foo(std::vector<int> v) {
  ...(vector書き換え)
  return v;
}
std::vector<int> vec1 = { 1, 2, 3 };
std::vector<int> vec2 = foo(vec1);

としてしまうとセマンティクスはいいものの、引数に渡したときにコピーが発生します。 速度と引き換えにImmutableな関数にしたい場合はこんな感じになっちゃうのかなと。 そのときは引数にconstつけてあげたほうが良いですね。
vectorが巨大な配列だった場合は前者の参照渡しにするしかないのかもしれません。

void foo(std::vector<int>& v) {
  ...(vector書き換え)
}
std::vector<int> vec1 = { 1, 2, 3 };
foo(vec1);

速度を求める場合はこうするしかなさそう。これはほかの言語でも結局こういう書き方になりますね。

この辺の話はここにまとまっていました。 …残念ながらC++を仕事で使うのは当分なさそうですが(T_T)

関連する話題

ふと、ここで、昔からどうするのがいいか悩んでいるところなのだけれど、 下記はJavaですが、
1.参照を更新

private void updateList(List<A> listOfA) {
  listOfA.forEach(item -> { 
    item.setProperty1(findSomething());
  }
}

2.参照を更新したものをreturn

private List<A> updateList(List<A> listOfA) {
  listOfA.forEach(item -> { 
    item.setProperty1(findSomething());
  }

  return listOfA;
}

3.新たなリストをreturn

private List<A> updateList(List<A> listOfA) {
  List<A> newList = new ArrayList<>();
  // Logic to copy listOfA in newList....
  newList.forEach(item -> { 
    item.setProperty1(findSomething());
  }
  return newList ;
}

3は状況に応じて最適ってわかるんですが、問題は1がいいのか2がいいのか… 2が個人的にはよく使っていたのですが、2だとおいおい、渡したやつも書き換わってるのかよ!って突っ込まれそうです。
2のメリットは書き換わっていることが書き方的にわかりやすいのと戻り値に直接メソッドを適用できてメソッドチェーンができるといったところでしょうか。でも無難に1にしようかなと迷うところです。
参考: java - Update parameters of a list in a method: return list or not - Stack Overflow
あーいやいや、ちゃんとオブジェクト指向をしていれば引数にListが渡ってくることはない?っていう気もしなくもない

あとがき

ちょっとだけ(本当にちょっとだけ)最近どうなってるの?っていうのを見てみたら、何やら色々増えてます。
気になったのが、
値のコピー省略を保証 - cpprefjp C++日本語リファレンス
で、そういえばRVO周りどうなってるんだ?っていうのがそもそもの発端です。