48

C++ で非常に基本的な質問があります。オブジェクトを返すときにコピーを避ける方法は?

例を次に示します。

std::vector<unsigned int> test(const unsigned int n)
{
    std::vector<unsigned int> x;
    for (unsigned int i = 0; i < n; ++i) {
        x.push_back(i);
    }
    return x;
}

C++ の仕組みを理解しているので、この関数は 2 つのベクトルを作成します。ローカルのベクトル (x) と、返される x のコピーです。コピーを回避する方法はありますか? (そして、オブジェクトへのポインターを返したくありませんが、オブジェクト自体)


「移動セマンティクス」(コメントに記載されている)を使用したその関数の構文は何ですか?

4

7 に答える 7

51

RVO (戻り値の最適化) がどのように機能するかについては、多少の混乱があるようです。

簡単な例:

#include <iostream>

struct A {
    int a;
    int b;
    int c;
    int d;
};

A create(int i) {
    A a = {i, i+1, i+2, i+3 };
    std::cout << &a << "\n";
    return a;
}

int main(int argc, char*[]) {
    A a = create(argc);
    std::cout << &a << "\n";
}

そしてideoneでの出力:

0xbf928684
0xbf928684

意外?

実際には、これが RVO の効果です。返されるオブジェクトは、呼び出し側で直接構築されます。

どのように ?

伝統的に、呼び出し元 (mainここ) は戻り値用にスタックにいくらかのスペースを確保します: return slot ; 呼び出し先(createここでは)には、戻り値をコピーする戻りスロットのアドレスが(何らかの方法で)渡されます。次に、呼び出し先は、他のローカル変数と同様に、結果を構築するローカル変数に独自のスペースを割り当て、それをreturnステートメントの戻りスロットにコピーします。

RVO は、同等のセマンティクス (as-if ルール) を使用して、変数をリターン スロットに直接構築できることをコンパイラがコードから推測したときにトリガーされます。

これは非常に一般的な最適化であるため、標準によって明示的にホワイトリストに登録されており、コンパイラはコピー (または移動) コンストラクターの副作用について心配する必要がないことに注意してください。

いつ ?

コンパイラは、次のような単純なルールを使用する可能性が最も高いです。

// 1. works
A unnamed() { return {1, 2, 3, 4}; }

// 2. works
A unique_named() {
    A a = {1, 2, 3, 4};
    return a;
}

// 3. works
A mixed_unnamed_named(bool b) {
    if (b) { return {1, 2, 3, 4}; }

    A a = {1, 2, 3, 4};
    return a;
}

// 4. does not work
A mixed_named_unnamed(bool b) {
    A a = {1, 2, 3, 4};

    if (b) { return {4, 3, 2, 1}; }

    return a;
}

後者の場合 (4)Aが返されたときに最適化を適用することはできません。これは、コンパイラがリターン スロットでビルドできないためです。aこれは、他の何かのために必要になる可能性があるためです (ブール条件によって異なりますb)。

したがって、簡単な経験則は次のとおりです。

RVO should be applied if no other candidate for the return slot has been declared prior to the return statement.

于 2012-05-07T09:22:37.060 に答える
25

このプログラムは、名前付き戻り値の最適化 (NRVO) を利用できます。ここを参照してください: http://en.wikipedia.org/wiki/Copy_elision

C++11 には、安価なムーブ コンストラクターと代入があります。ここでチュートリアルを読むことができます: http://thbecker.net/articles/rvalue_references/section_01.html

于 2012-05-07T04:37:02.237 に答える
14

名前付き戻り値の最適化、コンパイラーが使用中に冗長なCopyコンストラクターとDestructor呼び出しを排除しようとするため、その役割を果たします。

std::vector<unsigned int> test(const unsigned int n){
    std::vector<unsigned int> x;
    return x;
}
...
std::vector<unsigned int> y;
y = test(10);

戻り値の最適化あり:

  1. yが作成されます
  2. xが作成されます
  3. xはyに割り当てられます
  4. xが破壊される

(より深く理解するために自分で試してみたい場合は、この私の例を見てください)

またはさらに良いことに、Matthieu M.が指摘したように、宣言さtestれた同じ行内で呼び出す場合y、冗長オブジェクトの構築と冗長割り当ても回避できます(格納されるxメモリ内に構築されますy)。

std::vector<unsigned int> y = test(10);

その状況をよりよく理解するために彼の答えを確認してください(この種の最適化が常に適用できるとは限らないこともわかります)。

または、ベクトルの参照を関数に渡すようにコードを変更することもできます。これにより、コピーを回避しながら意味的に正確になります。

void test(std::vector<unsigned int>& x){
    // use x.size() instead of n
    // do something with x...
}
...
std::vector<unsigned int> y;
test(y);
于 2012-05-07T04:42:10.410 に答える
2

多くの場合、コンパイラは余分なコピーを最適化して削除できます (これは戻り値の最適化と呼ばれます)。https://isocpp.org/wiki/faq/ctors#return-by-value-optimizationを参照してください

于 2012-05-07T04:37:13.987 に答える
2

NRVO が発生しない場合、Move コンストラクターの使用が保証されます。

したがって、値による移動コンストラクター ( などstd::vector) を使用してオブジェクトを返す場合、コンパイラーがオプションの NRVO 最適化を実行できなかったとしても、完全なベクター コピーを実行しないことが保証されます。

これは、C++ 仕様自体に影響力があると思われる 2 人のユーザーによって言及されています。

有名人へのアピールに満足していませんか?

わかった。C++ 標準は完全には理解できませんが、その例は理解できます。;-)

C++17 n4659 標準ドラフト15.8.3 [class.copy.elision] "Copy/move elision"を引用

3 次のコピー初期化コンテキストでは、コピー操作の代わりに移動操作が使用される場合があります。

  • (3.1) — return ステートメント (9.6.3) の式が (括弧で囲まれている可能性がある) id-expression であり、最も内側の関数またはラムダの本体または parameter-declaration-clause で宣言された自動保存期間を持つオブジェクトを指定する場合-式、または
  • (3.2) — throw-expression (8.17) のオペランドが、スコープが最も内側の try-ブロック(ある場合)、

オブジェクトが右辺値によって指定されたかのように、コピーのコンストラクターを選択するためのオーバーロードの解決が最初に実行されます。最初のオーバーロードの解決が失敗したか実行されなかった場合、または選択されたコンストラクターの最初のパラメーターの型がオブジェクトの型への右辺値参照 (おそらく cv 修飾) でない場合、オブジェクトを左辺値。[ 注: この 2 段階のオーバーロード解決は、コピーの省略が発生するかどうかに関係なく実行する必要があります。省略が実行されない場合に呼び出されるコンストラクターを決定し、呼び出しが省略された場合でも、選択されたコンストラクターにアクセスできる必要があります。— エンドノート]

4 [ 例:

class Thing {
public:
  Thing();
  ~ Thing();
  Thing(Thing&&);
private:
  Thing(const Thing&);
};

Thing f(bool b) {
  Thing t;
  if (b)
    throw t;          // OK: Thing(Thing&&) used (or elided) to throw t
  return t;           // OK: Thing(Thing&&) used (or elided) to return t
}

Thing t2 = f(false);  // OK: no extra copy/move performed, t2 constructed by call to f

struct Weird {
  Weird();
  Weird(Weird&);
};

Weird g() {
  Weird w;
  return w;           // OK: first overload resolution fails, second overload resolution selects Weird(Weird&)
}

— 最後の例

「使用される可能性がある」という言い回しは好きではありませんが、「3.1」または「3.2」のいずれかが成立する場合、右辺値の戻りが発生する必要があることを意味すると思います。

これは、コードのコメントでかなり明確です。

参照渡し +std::vector.resize(0)複数の呼び出し

を複数回呼び出している場合、ベクターのサイズが 2 倍になると、いくつかの呼び出しと再配置コピーがtest節約されるため、これはわずかに効率的であると思います。malloc()

void test(const unsigned int n, std::vector<int>& x) {
    x.resize(0);
    x.reserve(n);
    for (unsigned int i = 0; i < n; ++i) {
        x.push_back(i);
    }
}

std::vector<int> x;
test(10, x);
test(20, x);
test(10, x);

https://en.cppreference.com/w/cpp/container/vector/resizeが言うことを考えると:

より小さなサイズにサイズ変更しても、ベクトルの容量が減ることはありません。これは、同等の pop_back() 呼び出しのシーケンスによって無効になるイテレータだけでなく、すべてのイテレータが無効になるためです。

また、余分なmallocを防ぐために、コンパイラが値バージョンによるリターンを最適化できるとは思いません。

一方、これは:

  • インターフェースを醜くする
  • ベクトル サイズを小さくすると、必要以上に多くのメモリが使用されます

したがって、トレードオフがあります。

于 2018-11-28T13:18:40.963 に答える
1

それを参照するとうまくいきます。

Void(vector<> &x) {

}
于 2012-05-07T08:04:18.177 に答える
-6

まず、戻り値の型を std::vector & に宣言することができます。この場合、コピーではなく参照が返されます。

ポインターを定義し、メソッド本体内にポインターを作成してから、そのポインター (または正しいポインターのコピー) を返すこともできます。

最後に、多くの C++ コンパイラは戻り値の最適化 (http://en.wikipedia.org/wiki/Return_value_optimization) を実行して一時オブジェクトを削除する場合があります。

于 2012-05-07T04:44:59.503 に答える