yohhoyの日記

技術的メモをしていきたい日記

参照渡し or 値渡し?

C++03/11における関数の引数型とコピー/ムーブ処理コストとの関係について。

本記事の内容は C++Now 2012 Keynote: "Moving Forward with C++11" スライド資料(Part I, Part II) に基づく。(Part IIのpp.22-57)

型Tに対する変更操作を行う関数において、引数の型をconst参照渡し(const T&)*1または値渡し(T)とするどちらが “良い” デザインかという話。ここでは関数呼び出しから値を返すまでに生じるコピーコンストラクタ/ムーブコンストラクタの呼び出し回数によって評価する。つまり、回数が少ない方が低コスト=良いデザインという観点にたつ。

// const参照渡し; pass-by-const-reference
T modify1(const T& x)
{
  T tmp(x);
  tmp.modify();
  return tmp;
}
// 値渡し; pass-by-value
T modify2(T x)
{
  x.modify();
  return x;
}

なおC++標準規格が許容する戻り値最適化(RVO; Return Value Optimization)/名前付き戻り値最適化(NRVO; Named Return Value Optimization)*2コンパイラによって常に行われるものと仮定する。

要約

  • C++03:通常はconst T&でよい。ただし型のサイズが小さくtrivialな場合はTの方が低コスト。(intなどの組込み型ではconst int&よりも単にintの方が低コスト。)
  • C++11:コピー操作よりも低コストなムーブ操作があり、かつ実引数が常にlvalueという使われ方でなければ、Tによる値渡しが有効。
  • C++11:const lvalue参照渡し(const T&)+rvalue参照渡し(T&&)の2種類をオーバーロード関数として提供すると、ムーブまたはコピー 1回 まで削減される。
  • 「常に値渡しを使うこと」や「値渡しは禁止」は共に正確でなく、関数インタフェース設計の問題である。

C++03

lvalue rvalue
const T& 1 copy 1 copy
T 2 copy 1 copy
T modify1(const T& x)
{
  T tmp(x);    // copy発生
  tmp.modify();
  return tmp;  // (NRVO)
}

T modify2(T x)  // lvalueのときcopy発生
{
  x.modify();
  return x;     // copy発生
}

T t0;
modify1(t0);   // lvalue
modify1(T());  // rvalue

C++11

lvalue xvalue prvalue
const T& 1 copy 1 copy 1 copy
T 1 copy
1 move
2 move 1 move
T modify1(const T& x)
{
  T tmp(x);    // copy発生
  tmp.modify();
  return tmp;  // (NRVO)
}

T modify2(T x)  // lvalueのときcopy発生、xvalueのときmove発生
{
  x.modify();
  return x;     // move発生
}

T t0;
modify1(t0);             // lvalue
modify1(std::move(t0));  // xvalue
modify1(T());            // prvalue

C++11: 関数オーバーロード

lvalue xvalue prvalue
const T& 1 copy 1 copy 1 copy
T 1 copy
1 move
2 move 1 move
const T&
T&&
1 copy 1 move 1 move
// [A] const lvalue参照(const T&)
T modify3(const T& x)
{
  T tmp(x);    // copy発生
  tmp.modify();
  return tmp;  // (NRVO)
}

// [B] rvalue参照(T&&)
T modify3(T&& x)
{
  x.modify();
  return std::move(x);  // move発生
}

T t0;
modify3(t0);             // lvalue  → [A]
modify3(std::move(t0));  // xvalue  → [B]
modify3(T());            // prvalue → [B]

*1:C++11では lvalue 参照と rvalue 参照は区別されるため、ここでは「const lvalue参照渡し」が正確。

*2:RVO/NRVOは関数戻り値のコピー省略による最適化手法の名称であり、C++11標準規格上は "copy elision" と表記している。(N3337 12.8/p31)