web-dev-qa-db-ja.com

「return」ステートメントでオブジェクトのコピーを回避する

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のコピーです。コピーを回避する方法はありますか? (そして、オブジェクトへのポインタを返したくありませんが、オブジェクト自体を返します)


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

36
Vincent

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

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

19
Pubby

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の効果です。オブジェクト返されるは、呼び出し元で直接作成されますin place

どうやって?

従来、呼び出し元(ここでは[main))は、戻り値用にスタック上のスペースを予約します。return slot;呼び出し先(create here)には、戻り値のコピー先の戻りスロットのアドレスが(なんらかの理由で)渡されます。次に、呼び出し先は、他のローカル変数の場合と同様に、結果を作成するローカル変数に独自のスペースを割り当て、それをreturnステートメントの戻りスロットにコピーします。

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

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

いつ?

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

// 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)。

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

returnステートメントの前に戻りスロットの候補が宣言されていない場合は、RVOを適用する必要があります。

38
Matthieu M.

名前付き戻り値の最適化 は、コンパイラが使用中に冗長なコピーコンストラクターとデストラクタコールを排除しようとするため、適切に機能します。

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を呼び出すと、冗長オブジェクトの構築を回避することもできます冗長な割り当ても(xyが格納されるメモリ内に構築されます):

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);
14
LihO

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

2
jamesdlin

移動コンストラクタは、NRVOが発生しない場合に使用されることが保証されています

したがって、値によってムーブコンストラクター(_std::vector_など)を使用してオブジェクトを返す場合、コンパイラーがオプションのNRVO最適化に失敗した場合でも、完全なベクトルコピーを行わないことが保証されます。

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

セレブリティへのアピールに満足していませんか?

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

C++ 17 n4659標準ドラフト 15.8.3 [class.copy.elision]「コピー/移動省略」を引用

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

  • (3.1)— returnステートメント(9.6.3)の式が、本体または最も内側の囲み関数またはラムダのparameter-declaration-clauseで宣言された自動ストレージ期間を持つオブジェクトを指定する(括弧で囲まれた)id式である場合-式、または
  • (3.2)— throw式(8.17)のオペランドが、スコープが最も内側の囲んでいるtry-の終わりを超えて拡張されていない(関数またはcatch-clauseパラメーター以外の)非揮発性自動オブジェクトの名前である場合ブロック(存在する場合)、

コピーのコンストラクタを選択するためのオーバーロード解決は、オブジェクトが右辺値で指定されているかのように最初に実行されます。最初のオーバーロード解決が失敗または実行されなかった場合、または選択されたコンストラクターの最初のパラメーターのタイプがオブジェクトのタイプへの右辺値参照でない場合(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)

testを複数回呼び出す場合、ベクトルのサイズが2倍になったときに、いくつかの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を防ぐことができないと思います。

一方、これは:

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

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

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

Void(vector<> &x) {

}
1
Jake Runzer