値渡しとstd :: moveが参照渡しよりも優れている点


105

私は現在C ++を学んでおり、悪い習慣を身につけないようにしています。私が理解していることから、clang-tidyには多くの「ベストプラクティス」が含まれており、可能な限り最善を尽くすように努めていますが(なぜそれらがまだ優れていると見なされているのは必ずしも理解ていません)、私は確信が持てませんここで何が推奨されているかを理解してください。

私はチュートリアルからこのクラスを使用しました:

class Creature
{
private:
    std::string m_name;

public:
    Creature(const std::string &name)
            :  m_name{name}
    {
    }
};

これは、参照ではなく値で渡し、を使用する必要があるというclang-tidyからの提案につながりますstd::move。実行するとname、参照を作成するように提案され(毎回コピーされないようにするため)、警告が表示さstd::moveれるので影響がないためnameconst削除する必要があります。

警告を受け取らない唯一の方法は、const完全に削除することです。

Creature(std::string name)
        :  m_name{std::move(name)}
{
}

唯一の利点はconst元の文字列をいじるのを防ぐことだったので、これは理にかなっているように見えます(値で渡したため発生しません)。しかし、私はCPlusPlus.comで読みました:

ただし、標準ライブラリでは、移動は移動元のオブジェクトが有効ですが指定されていない状態のままであることを意味します。つまり、そのような操作の後、移動されたオブジェクトの値は破棄されるか、新しい値が割り当てられるだけです。それ以外の方法でアクセスすると、未指定の値が生成されます。

このコードを想像してみてください:

std::string nameString("Alex");
Creature c(nameString);

nameString値で渡されるため、コンストラクター内でstd::moveのみ無効化さnameれ、元の文字列には触れません。しかし、これの利点は何ですか?とにかくコンテンツが一度だけコピーされるようです-を呼び出すときに参照m_name{name}渡しする場合、渡すときに値渡しする場合(そして移動される場合)。これは、値を渡して使用しないよりも優れていることを理解していますstd::move(2回コピーされるため)。

したがって、2つの質問:

  1. ここで何が起こっているのかを正しく理解しましたか?
  2. std::move参照渡しを使用して呼び出すだけのメリットはありm_name{name}ますか?

3
参照渡しでCreature c("John");、追加のコピーを作成
user253751

1
このリンクは貴重な参考になるかもしれませstd::string_viewん。合格とSSOについても説明しています。
lubgr 2018

私はclang-tidy、読みやすさを犠牲にして、不要なマイクロ最適化に取り付かれるのに最適な方法であることがわかりました。ここで質問するのは、何よりもまず、実際Creatureコンストラクタを呼び出す回数です。
CZ

回答:


36
  1. ここで何が起こっているのかを正しく理解しましたか?

はい。

  1. std::move参照渡しを使用して呼び出すだけのメリットはありm_name{name}ますか?

追加のオーバーロードなしで把握しやすい関数シグネチャ。シグネチャは、引数がコピーされることをすぐに明らかにします-これにより、呼び出し元がconst std::string&参照がデータメンバーとして保存され、後でぶら下がっている参照になる可能性があるかどうか疑問に思うのを防ぎます。そして、右辺値が関数に渡されるときに不要なコピーを回避するためにstd::string&& nameconst std::string&引数をオーバーロードする必要はありません。左辺値を渡す

std::string nameString("Alex");
Creature c(nameString);

引数を値として取る関数に対して、1つのコピーと1つの移動の構築を引き起こします。右辺値を同じ関数に渡す

std::string nameString("Alex");
Creature c(std::move(nameString));

2つの移動コンストラクションが発生します。対照的に、関数パラメーターがのconst std::string&場合、右辺値引数を渡しても、常にコピーが存在します。これは、引数の型がmove-constructに安価である限り、明らかに有利です(これはの場合ですstd::string)。

ただし、考慮すべき欠点があります。関数の引数を(初期化する代わりに)別の変数に割り当てる関数の場合、この推論は機能しません。

void setName(std::string name)
{
    m_name = std::move(name);
}

m_name再割り当てされる前に参照するリソースの割り当てを解除します。「Effective Modern C ++」の項目41 とこの質問もお勧めします。


これは理にかなっています。特に、これにより宣言がより直感的に読めるようになります。あなたの回答の割り当て解除の部分を完全に把握している(そしてリンクされたスレッドを理解している)とは思えないので、チェックするだけmoveです。を使用しないmove場合、割り当てられたスペースが小さすぎて新しい文字列を保持できない場合にのみ割り当てが解除され、パフォーマンスが向上します。あれは正しいですか?
ブラックボット2018

1
はい、それだけです。パラメータm_nameから代入する場合const std::string&は、内部メモリがm_name収まる限り再利用します。移動代入するm_name場合は、あらかじめメモリの割り当てを解除する必要があります。それ以外の場合は、割り当ての右側からリソースを「盗む」ことは不可能でした。
lubgr

いつぶら下がりリファレンスになるのですか?初期化リストはディープコピーを使用していると思います。
Li Taiji

100
/* (0) */ 
Creature(const std::string &name) : m_name{name} { }
  • 渡された左辺値はにバインドされname、次ににコピーさm_nameます。

  • 渡された右辺値はにバインドされname、次ににコピーさm_nameます。


/* (1) */ 
Creature(std::string name) : m_name{std::move(name)} { }
  • 渡された左辺値はにコピーさname、次にに移動されm_nameます。

  • 渡された右辺値はに移動されname、次にに移動されm_nameます。


/* (2) */ 
Creature(const std::string &name) : m_name{name} { }
Creature(std::string &&rname) : m_name{std::move(rname)} { }
  • 渡された左辺値はにバインドされname、次ににコピーさm_nameます。

  • 渡された右辺値はにバインドされrname、次にに移動されm_nameます。


通常、移動操作はコピーよりも高速であるため、多数の一時ファイルを渡す場合は(1)(0)よりも優れています。(2)コピー/移動の点では最適ですが、コードの繰り返しが必要です。

コードの繰り返しは、完全な転送で回避できます。

/* (3) */
template <typename T,
          std::enable_if_t<
              std::is_convertible_v<std::remove_cvref_t<T>, std::string>, 
          int> = 0
         >
Creature(T&& name) : m_name{std::forward<T>(name)} { }

オプションでT、このコンストラクターをインスタンス化できる型のドメインを制限するために制約することもできます(上記を参照)。C ++ 20はConceptsでこれを単純化することを目指しています。


C ++ 17では、prvalue保証れたコピー省略によって影響を受けます。これにより、関数に引数を渡すときにコピー/移動の数が削減されます(該当する場合)。


(1)の場合、pr値とx値のケースは、c ++ 17から同じではありませんか?
Oliv

1
この場合、SFINAEで完全に転送する必要ありません。明確にするためだけに必要です。これは、
不適切

@Olivはい。xvaluesは移動する必要がありますが、prvaluesは省略できます:)
Rakete1111

1
私たちは書くことができますCreature(const std::string &name) : m_name{std::move(name)} { }(2)
スカイツリー

4
@skytree:移動するとソースが変化するため、constオブジェクトから移動することはできません。それはコンパイルされますが、コピーが作成されます。
Vittorio Romeo、

1

渡す方法はここでの唯一の変数ではなく、渡すものは2つの間の大きな違いになります。

C ++では、すべての種類の値カテゴリがあり、この「イディオム」は右辺値"Alex-string-literal-that-constructs-temporary-std::string"またはなどstd::move(nameString))を渡す場合に存在します。これにより、コピーの作成0std::stringになります(タイプはコピー構築可能である必要もありません)右辺値引数の場合)、およびstd::stringの移動コンストラクタのみを使用します。

やや関連のあるQ&A


1

pass-by-(rv)referenceに比べて、pass-by-value-and-moveアプローチにはいくつかの欠点があります。

  • 2つではなく3つのオブジェクトが生成されます。
  • オブジェクトを値で渡すと、通常の文字列クラスでも通常はポインターの3倍または4倍の大きさになるため、スタックのオーバーヘッドが増える可能性があります。
  • 引数オブジェクトの構築は呼び出し側で行われるため、コードが膨張します。

3つのオブジェクトがスポーンする原因を明確にしていただけませんか?私が理解していることから、「Peter」を文字列として渡すことができます。これはスポーンされ、コピーされてから移動されますね。とにかく、スタックはどこかで使用されませんか?コンストラクタ呼び出しの時点ではなく、m_name{name}コピーされる部分ですか?
ブラックボット2018

@Blackbot私はあなたの例を参照していましたstd::string nameString("Alex"); Creature c(nameString);nameString、1つは、もう1つは関数の引数、3つ目はクラスフィールドです。
user7860670

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.