C ++でstd :: vectorを返す効率的な方法


106

関数でstd :: vectorを返すときにコピーされるデータの量、およびstd :: vectorを(ヒープ上の)フリーストアに配置して、代わりにポインターを返す最適化の大きさは次のとおりです。

std::vector *f()
{
  std::vector *result = new std::vector();
  /*
    Insert elements into result
  */
  return result;
} 

よりも効率的:

std::vector f()
{
  std::vector result;
  /*
    Insert elements into result
  */
  return result;
} 


3
参照でベクトルを渡して、それを内部に埋めてみfませんか?
Kiril Kirov

4
RVOはかなり基本的な最適化であり、ほとんどのコンパイラーはいつでも実行できます。
Remus Rusanu 2013年

回答が流れてくると、C ++ 03とC ++ 11のど​​ちらを使用しているかを明確にするのに役立ちます。2つのバージョン間のベストプラクティスはかなり異なります。
Drew Dormann 2013年


@Kiril Kirov、それを関数の引数リストに入れずにそれを行うことはできますか?void f(std :: vector&result)?
Morten

回答:


140

C ++ 11では、これが推奨される方法です。

std::vector<X> f();

つまり、値で返します。

C ++ 11にstd::vectorは、移動セマンティクスがあります。つまり、地元のベクトルはあなたの関数内で宣言がされます移動し、リターンに、いくつかのケースでも、動きがコンパイラによって省略させることができます。


13
@LeonidVolnitsky:ローカルであれば可能です。実際、return std::move(v);だけで可能だったとしても、移動省略は無効になりreturn v;ます。したがって、後者が推奨されます。
Nawaz 2013年

1
@juanchopanza:そうは思いません。C ++ 11以前は、ベクトルが移動されないため、これに反対する可能性がありました。RVOはコンパイラに依存するものです。80年代と90年代のことについて話します。
Nawaz 2018年

2
(値による)戻り値についての私の理解は、「移動される」のではなく、呼び出し先の戻り値が呼び出し元のスタックに作成されるため、呼び出し先のすべての操作が適切に行われ、RVOで移動するものはありません。あれは正しいですか?
r0ng 2018

2
@ r0ng:はい、そうです。これは、コンパイラが通常RVOを実装する方法です。
Nawaz

1
@Nawazそうではありません。もはや動きさえありません。
オービットでの軽量レース

70

値で返す必要があります。

この規格には、値による返却の効率を向上させる特定の機能があります。これは「コピー削除」と呼ばれ、より具体的には、この場合は「名前付き戻り値の最適化(NRVO)」と呼ばれます。

コンパイラはそれを実装する必要はありませんが、再びコンパイラ、関数のインライン化を実装する(または最適化を実行する)。しかし、コンパイラーが最適化しない場合、標準ライブラリーのパフォーマンスはかなり低下する可能性があり、すべての深刻なコンパイラーはインライン化とNRVO(およびその他の最適化)を実装します。

NRVOを適用すると、次のコードでコピーは行われません。

std::vector<int> f() {
    std::vector<int> result;
    ... populate the vector ...
    return result;
}

std::vector<int> myvec = f();

しかし、ユーザーはこれを実行したいかもしれません:

std::vector<int> myvec;
... some time later ...
myvec = f();

これは初期化ではなく割り当てであるため、コピーの省略はここでのコピーを妨げません。ただし、値で返す必要があります。C ++ 11では、割り当ては「移動セマンティクス」と呼ばれる別の方法で最適化されます。C ++ 03では、上記のコードはコピーを引き起こし、理論的にはオプティマイザがそれを回避できるかもしれませんが、実際にはそれは非常に困難です。したがってmyvec = f()、C ++ 03では、次のように書く必要があります。

std::vector<int> myvec;
... some time later ...
f().swap(myvec);

より柔軟なインターフェースをユーザーに提供するという別のオプションがあります。

template <typename OutputIterator> void f(OutputIterator it) {
    ... write elements to the iterator like this ...
    *it++ = 0;
    *it++ = 1;
}

さらに、その上にある既存のベクターベースのインターフェースをサポートすることもできます。

std::vector<int> f() {
    std::vector<int> result;
    f(std::back_inserter(result));
    return result;
}

これ、既存のコードがreserve()固定額よりも複雑な方法で前もって使用する場合、既存のコードよりも効率が悪い場合があります。ただし、既存のコードが基本的にpush_backベクターを繰り返し呼び出す場合、このテンプレートベースのコードも同様に優れているはずです。


本当に最善かつ詳細な回答に賛成しました。ただし、swap()バリアントでは(NRVOのないC ++ 03の場合)、f()内にコピーコンストラクターコピーが1つ作成されます。変数の結果から、最後にmyvecにスワップされる非表示の一時オブジェクトにコピーされます。
JenyaKh 2017

@JenyaKh:確かに、それは実装品質の問題です。標準では、C ++ 03実装が関数のインライン化を必要としないのと同じように、NRVOを実装する必要がありませんでした。関数のインライン化との違いは、NRVOが変更するのに対して、インライン化はセマンティクスやプログラムを変更しないことです。ポータブルコードは、NRVOの有無にかかわらず機能する必要があります。特定の実装(および特定のコンパイラフラグ)に最適化されたコードは、実装独自のドキュメントでNRVOに関する保証を求めることができます。
スティーブジェソップ2017

3

RVOについての回答を投稿するときが来ました。私もです...

値でオブジェクトを返す場合、コンパイラーはこれを最適化することがよくあり、一時的に関数内でオブジェクトを作成してからコピーする必要がないため、2回作成されません。これは、戻り値の最適化と呼ばれます。作成されたオブジェクトは、コピーされる代わりに移動されます。


1

C ++ 11以前の一般的なイディオムは、埋められるオブジェクトへの参照を渡すことです。

その後、ベクターのコピーはありません。

void f( std::vector & result )
{
  /*
    Insert elements into result
  */
} 

3
これは、C ++ 11ではもはや慣用句ではありません。
Nawaz 2013年

1
@Nawaz同意する。C ++についての質問に関して、SOでのベストプラクティスが今何であるかはわかりませんが、特にC ++ 11についてはわかりません。私はC ++ 11の答えを学生に、C ++ 03の答えをウエストの深い本番コードの誰かに与える傾向があると思います。意見はありますか?
Drew Dormann 2013年

7
実際、C ++ 11のリリース後(19か月前)、C ++ 03の質問であると明記されていない限り、すべての質問はC ++ 11の質問であると考えています。
Nawaz 2013年

1

コンパイラが名前付き戻り値の最適化(http://msdn.microsoft.com/en-us/library/ms364057(v=vs.80).aspx)をサポートしている場合は、次の条件が満たされていなければ、ベクトルを直接返すことができます。

  1. 別の名前付きオブジェクトを返す別のパス
  2. EH状態が導入された複数のリターンパス(同じ名前のオブジェクトがすべてのパスで返される場合でも)。
  3. 返された名前付きオブジェクトは、インラインasmブロックで参照されます。

NRVOは、冗長なコピーコンストラクターとデストラクタコールを最適化し、全体的なパフォーマンスを向上させます。

あなたの例では実際の違いはないはずです。


0
vector<string> getseq(char * db_file)

そして、main()で印刷したい場合は、ループで実行する必要があります。

int main() {
     vector<string> str_vec = getseq(argv[1]);
     for(vector<string>::iterator it = str_vec.begin(); it != str_vec.end(); it++) {
         cout << *it << endl;
     }
}

-2

「値による戻り」と同じくらい素晴らしいのは、エラーにつながる可能性がある種類のコードです。次のプログラムを考えてみましょう:

    #include <string>
    #include <vector>
    #include <iostream>
    using namespace std;
    static std::vector<std::string> strings;
    std::vector<std::string> vecFunc(void) { return strings; };
    int main(int argc, char * argv[]){
      // set up the vector of strings to hold however
      // many strings the user provides on the command line
      for(int idx=1; (idx<argc); ++idx){
         strings.push_back(argv[idx]);
      }

      // now, iterate the strings and print them using the vector function
      // as accessor
      for(std::vector<std::string>::interator idx=vecFunc().begin(); (idx!=vecFunc().end()); ++idx){
         cout << "Addr: " << idx->c_str() << std::endl;
         cout << "Val:  " << *idx << std::endl;
      }
    return 0;
    };
  • Q:上記を実行するとどうなりますか?A:コアダンプ。
  • Q:コンパイラーが間違いを見つけなかったのはなぜですか?A:プログラムは構文的には、意味的にはではないが正しいので。
  • Q:vecFunc()を変更して参照を返すとどうなりますか?A:プログラムは最後まで実行され、期待される結果が得られます。
  • Q:違いは何ですか?A:コンパイラは匿名オブジェクトを作成および管理する必要はありません。プログラマーは、壊れた例のように2つの異なるオブジェクトではなく、イテレーターとエンドポイントの決定に1つのオブジェクトを使用するようにコンパイラーに指示しました。

上記のエラーのあるプログラムは、GNU g ++レポートオプション-Wall -Wextra -Weffc ++を使用しても、エラーがないことを示します。

値を生成する必要がある場合は、vecFunc()を2回呼び出す代わりに、以下が機能します。

   std::vector<std::string> lclvec(vecFunc());
   for(std::vector<std::string>::iterator idx=lclvec.begin(); (idx!=lclvec.end()); ++idx)...

上記はまた、ループの反復中に匿名オブジェクトを生成しませんが、可能なコピー操作が必要です(これは、一部の状況では最適化される可能性があります。ただし、参照メソッドは、コピーが生成されないことを保証します。コンパイラーは、実行RVOは、できる限り効率的なコードを作成しようとするのに代わるものではありません。


3
これは、ユーザーがC ++に慣れていない場合に問題が発生する可能性があることを示す例です。.netやjavascriptなどのオブジェクトベースの言語に精通している人は、文字列ベクトルが常にポインターとして渡されると想定しているため、この例では常に同じオブジェクトを指すことになります。vecfunc()。begin()とvecfunc()。end()は文字列ベクトルのコピーであるため、この例では必ずしも一致しません。
メドラン2018年

-2
   vector<string> func1() const
   {
      vector<string> parts;
      return vector<string>(parts.begin(),parts.end()) ;
   } 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.