私は NRVO について読んでいて、NRVO に頼るべきときとそうでないときを理解しようとしていました。ここで質問があります。なぜ NRVO に頼る必要があるのでしょうか。戻りパラメータを明示的に参照渡しすることは常に可能ですが、代わりに NRVO に依存する理由はありますか?
4 に答える
戻り値の処理は、参照パラメーターへの書き込みによって返されるメソッドを処理するよりも簡単です。次の2つの方法を検討してください
C GetByRet() { ... }
void GetByParam(C& returnValue) { ... }
最初の問題は、メソッド呼び出しの連鎖が不可能になることです
Method(GetByRet());
// vs.
C temp;
GetByParam(temp);
Method(temp);
また、使用できないなどの機能も作成auto
します。のような型にとってはそれほど問題ではありませんが、のような型C
にとってはより重要ですstd::map<std::string, std::list<std::string>*>
auto ret = GetByRet();
// vs.
auto value; // Error!
GetByParam(value);
また、GMacNickG が指摘したように、C
通常のコードでは使用できないプライベート コンストラクターが型に含まれている場合はどうなるでしょうか。コンストラクターがprivate
存在するか、デフォルトのコンストラクターが存在しない可能性があります。もう一度GetByRet
チャンピオンのように機能し、GetByParam
失敗します
C ret = GetByRet(); // Score!
// vs.
C temp; // Error! Can't access the constructor
GetByParam(temp);
これは答えではありませんが、ある意味では答えでもあります...
ポインターによって引数を取る関数が与えられた場合、値によって戻り、コンパイラーによって簡単に最適化できる関数を生成する簡単な変換があります。
void f(T *ptr) {
// uses ptr->...
}
関数内のオブジェクトへの参照を追加し、ptr のすべての使用を参照に置き換えます
void f(T *ptr) { T & obj = *ptr; /* uses obj. instead of ptr-> */ }
引数を削除し、戻り値の型を追加し、すべての戻り値を yield 'obj'に置き換え
T& obj
て変更しますT obj
T f() { T obj; // No longer a ref! /* code does not change */ return obj; }
この時点で、すべての return ステートメントが同じオブジェクトを参照するため、NRVO が簡単な値で返す関数ができました。
この変換された関数には、ポインターによる受け渡しと同じ欠点がいくつかありますが、それよりも悪いことはありません。しかし、ポインターによる受け渡しがオプションであるときはいつでも、値による戻りも同じコストのオプションであることを示しています。
まったく同じ費用?
これは言語を超えていますが、コンパイラがコードを生成するときは、コンパイラのさまざまな実行 (または同じプラットフォーム内のさまざまなコンパイラ) によるコードのビルドを可能にする ABI (アプリケーション バイナリ インターフェイス) に従ってコードを生成します。現在使用されているすべての ABI は、値関数による戻り値の共通の特性を共有しています。大きな(レジスタに収まらない) 戻り値の型の場合、返されるオブジェクトのメモリは呼び出し元によって割り当てられ、関数はそのメモリの場所を持つ追加のポインターを受け取ります。 . それは、コンパイラが見たときです
T f();
呼び出し規約はそれを次のように変換します。
void mangled_name_for_f( T* __result )
したがって、代替案を比較するT t; f(&t);
とT t = f();
、どちらの場合も、生成されたコードは呼び出し元のフレーム [1] にスペースを割り当て、ポインターを渡す関数を呼び出します。関数の最後で、コンパイラは [2] を返します。[#] は、オブジェクトのコンストラクターが各選択肢で実際に呼び出される場所です。両方の選択肢のコストは同じですが、[1] ではオブジェクトをデフォルトで構築する必要がありますが、[2] ではオブジェクトの最終的な値を既に知っている可能性があり、より効率的な方法を実行できる可能性があります。
性能に関しては、それだけですか?
あまり。後でそのオブジェクトを値渡しの引数を取る関数に渡す必要があるvoid g(T value)
場合、たとえば、ポインター渡しの場合、呼び出し元のスタックに名前付きオブジェクトがあるため、オブジェクトをにコピー (または移動) する必要があります。呼び出し規約で value 引数が必要な場所。値による戻りの場合、コンパイラは、それが呼び出されるg(f())
ことを認識しており、返されたオブジェクトの唯一の使用はf()
の引数であることを認識しているため、 をg()
呼び出すときに適切な場所へのポインタを渡すことができます。f()
コピーは行わないでください。この時点で、の実装が上記のばかげた変換を使用している場合でも、手動のアプローチはコンパイラーのアプローチに大きく遅れを取り始めます!f
T obj; // default initialize
f(&obj); // assign (or modify in place)
g(obj); // copy
g(f()); // single object is returned and passed to g(), no copies
常に参照によって値を返すことは、実際には不可能 (または望ましい) ではありません (operator+
基本的な反例として考えてください)。
あなたの質問に答えるには: 通常、NRVO が常に発生することに依存したり、期待したりすることはありませんが、コンパイラが適切な最適化を行うことを期待しています。プロファイリングが戻り値のコピーが高価であることを示している場合にのみ、ヒントや代替インターフェースでコンパイラーを支援することを心配する必要があります。
編集some function could be optimized just by using return parameter
:
まず、関数が頻繁に呼び出されない場合、またはコンパイラに十分なスマートがある場合、out-parameter による return が最適化されていることを保証できないことを覚えておいてください。第二に、コードの将来のメンテナーがいるということ、そして明確で理解しやすいコードを書くことは、あなたが提供できる最大の支援の 1 つであることを覚えておいてください (壊れたコードがどれほど速いかは問題ではありません)。3 番目に、少し時間を取ってhttp://cpp-next.com/archive/2009/08/want-speed-pass-by-value/を読み、考えが変わるかどうかを確認してください。
非 const 参照パラメーターを関数に渡し、関数内でそれらのパラメーターを変更することはあまり直感的ではないと多くの人が主張しています。
また、結果を値で返す定義済みの演算子が多数あります (例: 、 などの算術演算子operator+
) operator-
。このような演算子のデフォルトのセマンティクス (および署名) を維持したいので、値によって返される一時オブジェクトを最適化するために NRVO に頼らざるを得ません。
最後に、値で返すと、多くの場合、非 const 参照 (またはポインター) によって変更されるパラメーターを渡すよりも簡単に連鎖できます。