web-dev-qa-db-ja.com

移動セマンティクスをサポートするには、関数パラメーターをunique_ptr、value、またはrvalueのいずれかで取得する必要がありますか?

私の関数の1つは、ベクトルをパラメーターとして受け取り、それをメンバー変数として格納します。以下に説明するように、ベクトルへのconst参照を使用しています。

class Test {
 public:
  void someFunction(const std::vector<string>& items) {
   m_items = items;
  }

 private:
  std::vector<string> m_items;
};

ただし、itemsに多数の文字列が含まれている場合があるため、移動セマンティクスをサポートする関数を追加(または関数を新しい関数に置き換え)したいと思います。

私はいくつかのアプローチを考えていますが、どれを選ぶべきかわかりません。

1)unique_ptr

void someFunction(std::unique_ptr<std::vector<string>> items) {
   // Also, make `m_itmes` std::unique_ptr<std::vector<string>>
   m_items = std::move(items);
}

2)値を渡して移動する

void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

3)右辺値

void someFunction(std::vector<string>&& items) {
   m_items = std::move(items);
}

どのアプローチを避けるべきですか、そしてその理由は何ですか?

16
MaxHeap

ベクターがヒープ上に存在する理由がない限り、_unique_ptr_の使用はお勧めしません。

ベクトルの内部ストレージはとにかくヒープ上に存在するため、_unique_ptr_を使用する場合は、2度の間接参照が必要になります。1つはベクトルへのポインターを逆参照し、もう1つは内部ストレージバッファーを逆参照します。

そのため、2または3のいずれかを使用することをお勧めします。

オプション3(右辺値参照が必要)を使用する場合、someFunctionを呼び出すときに、クラスのユーザーに右辺値を渡す(一時値から直接、または左辺値から移動する)という要件を課しています。

左辺値から移動する要件は面倒です。

ユーザーがベクターのコピーを保持したい場合は、フープを飛び越えてそうする必要があります。

_std::vector<string> items = { "1", "2", "3" };
Test t;
std::vector<string> copy = items; // have to copy first
t.someFunction(std::move(items));
_

ただし、オプション2を選択した場合、ユーザーはコピーを保持するかどうかを決定できます。選択は自分で行います。

コピーを保管してください:

_std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(items); // pass items directly - we keep a copy
_

コピーを保持しないでください:

_std::vector<string> items = { "1", "2", "3" };
Test t;
t.someFunction(std::move(items)); // move items - we don't keep a copy
_
32
Steve Lorimer

表面的には、オプション2は、左辺値と右辺値の両方を1つの関数で処理するため、良いアイデアのように思われます。ただし、ハーブサッターがCppCon 2014の講演で述べているように、 基本に戻る!現代のC++スタイルの要点これは、左辺値の一般的なケースの悲観論です。

m_itemsitemsよりも「大きい」場合、元のコードはベクトルにメモリを割り当てません。

// Original code:
void someFunction(const std::vector<string>& items) {
   // If m_items.capacity() >= items.capacity(),
   // there is no allocation.
   // Copying the strings may still require
   // allocations
   m_items = items;
}

std::vectorのコピー代入演算子は、既存の代入を再利用するのに十分賢いです。一方、パラメータを値で取得するには、常に別の割り当てを行う必要があります。

// Option 2:
// When passing in an lvalue, we always need to allocate memory and copy over
void someFunction(std::vector<string> items) {
   m_items = std::move(items);
}

簡単に言うと、コピーの作成と割り当てのコストは必ずしも同じではありません。コピーの割り当てがコピーの作成よりも効率的である可能性は低くありません—std::vectorおよびstd::stringの方が効率的です。 

Herbが指摘しているように、最も簡単な解決策は、右辺値のオーバーロードを追加することです(基本的にはオプション3)。

// You can add `noexcept` here because there will be no allocation‡
void someFunction(std::vector<string>&& items) noexcept {
   m_items = std::move(items);
}

コピー割り当ての最適化は、m_itemsがすでに存在する場合にのみ機能するため、値によるコンストラクターへのパラメーターの取得はまったく問題ありません。割り当てはどちらの方法でも実行する必要があります。

TL; DR:addオプション3を選択します。つまり、左辺値に対して1つのオーバーロードと1つのオーバーロードがあります。右辺値の場合。オプション2は、コピー割り当ての代わりにコピー構築を強制します。これは、よりコストがかかる可能性があります(std::stringおよびstd::vector用です)

†オプション2が悲観的である可能性があることを示すベンチマークを見たい場合、 話のこの時点で 、ハーブはいくつかのベンチマークを示しています

std::vectorのムーブ代入演算子がnoexceptでない場合、これをnoexceptとしてマークするべきではありませんでした。カスタムアロケータを使用している場合は、 ドキュメント を参照してください。
経験則として、タイプのムーブ代入がnoexceptの場合にのみ、同様の関数にnoexceptのマークを付ける必要があることに注意してください。

15
Justin

それはあなたの使用パターンに依存します:

オプション1

長所:

  • 責任は明示的に表現され、発信者から着信者に渡されます

短所:

  • ベクトルがすでに_unique_ptr_を使用してラップされていない限り、これによって読みやすさが向上することはありません。
  • 一般に、スマートポインターは、動的に割り当てられたオブジェクトを管理します。したがって、vectorは1つになる必要があります。標準ライブラリコンテナは、値の格納に内部割り当てを使用する管理対象オブジェクトであるため、これは、そのようなベクトルごとに2つの動的割り当てがあることを意味します。 1つは一意のptr + vectorオブジェクト自体の管理ブロック用で、もう1つは保存されたアイテム用です。

概要:

_unique_ptr_を使用してこのベクトルを一貫して管理している場合は、引き続き使用してください。そうでない場合は使用しないでください。

オプション2

長所:

  • このオプションは、発信者がコピーを保持しないかどうかを決定できるため、非常に柔軟性があります。

    _std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(vec); // vec stays a valid copy
    t.someFunction(std::move(vec)); // vec is moved
    _
  • 呼び出し元がstd::move()を使用すると、オブジェクトは2回だけ移動されます(コピーはありません)。これは効率的です。

短所:

  • 呼び出し元がstd::move()を使用しない場合、一時オブジェクトを作成するために常にコピーコンストラクターが呼び出されます。 void someFunction(const std::vector<std::string> & items)を使用し、_m_items_がすでにitemsを収容するのに十分な大きさであった場合、割り当て_m_items = items_は追加の割り当てなしでのコピー操作。

概要:

このオブジェクトがreになることを事前に知っている場合-実行時に何度も設定され、呼び出し元は常にstd::move()、私はそれを避けていただろう。それ以外の場合、これは非常に柔軟性があり、問題のあるシナリオにもかかわらず、要求に応じて使いやすさと高いパフォーマンスの両方を可能にするため、優れたオプションです。

オプション3

短所:

  • このオプションは、発信者に自分のコピーをあきらめることを強制します。したがって、自分自身にコピーを保持したい場合は、追加のコードを作成する必要があります。

    _std::vector<std::string> vec { ... };
    Test t;
    t.someFunction(std::vector<std::string>{vec});
    _

概要:

これはオプション#2よりも柔軟性が低いため、ほとんどのシナリオで劣っていると思います。

オプション4

オプション2と3の短所を考えると、追加のオプションを提案すると思います。

_void someFunction(const std::vector<int>& items) {
    m_items = items;
}

// AND

void someFunction(std::vector<int>&& items) {
    m_items = std::move(items);
}
_

長所:

  • オプション2と3で説明したすべての問題のあるシナリオを解決すると同時に、それらの利点も享受します。
  • 発信者は自分自身にコピーを保持するかどうかを決定しました
  • 特定のシナリオに合わせて最適化できます

短所:

概要:

そのようなプロトタイプがない限り、これは素晴らしいオプションです。

7
Daniel Trugman

これに関する現在のアドバイスは、ベクトルを値で取得し、それをメンバー変数に移動することです。

_void fn(std::vector<std::string> val)
{
  m_val = std::move(val);
}
_

そして、私はちょうどチェックしました、_std::vector_はムーブ代入演算子を提供します。呼び出し元がコピーを保持したくない場合は、呼び出しサイトの関数fn(std::move(vec));に移動できます。

0
Andre Kostur