web-dev-qa-db-ja.com

std :: moveは、値で渡される重いメンバーのコンストラクターの初期化リストで本当に必要ですか?

最近、私は cppreference .../vector/emplace_back から例を読みました:

struct President
{
    std::string name;
    std::string country;
    int year;

    President(std::string p_name, std::string p_country, int p_year)
        : name(std::move(p_name)), country(std::move(p_country)), year(p_year)
    {
        std::cout << "I am being constructed.\n";
    }

私の質問:これは本当にstd::moveが必要ですか?私の要点は、このp_nameはコンストラクターの本体では使用されないということです。おそらく、言語には移動のセマンティクスをデフォルトで使用するルールがあるのでしょうか?

これは、初期化リストにstd :: moveをすべての重いメンバー(std::stringstd::vectorなど)に追加するのは本当に面倒です。 C++ 03で書かれた何百ものKLOCプロジェクトを想像してみてください-これをどこにでも追加しますかstd::move

この質問: move-constructor-and-initialization-list 答えは言う:

ゴールデンルールとして、右辺値参照で何かを取る場合は常にstd :: move内で使用する必要があり、ユニバーサル参照(つまり&&を使用した推定テンプレート型)で使用する場合は常にstd ::内で使用する必要があります。フォワード

しかし、私は確信がありません。値渡しはむしろ普遍的な参照ではありませんか?

[UPDATE]

私の質問をより明確にするため。コンストラクタの引数をXValueとして扱うことはできますか?値の期限切れを意味しますか?

この例のAFAIKでは、std::moveを使用していません。

std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}

それで、ここでそれが必要でしょうか:

void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}

私にとって、このローカル変数は期限切れの変数です-したがって、移動のセマンティクスを適用できます...これは、値で渡される引数に似ています...

35
PiotrNycz

私の質問:このstd :: moveは本当に必要ですか?私のポイントは、コンパイラーがこのp_nameがコンストラクターの本体で使用されていないことを認識していることです。おそらく、デフォルトで移動セマンティクスを使用するいくつかのルールがありますか?

一般に、左辺値を右辺値に変換したい場合、はい、std::move()が必要です。参照 C++ 11コンパイラは、コードの最適化時にローカル変数を右辺値に変換できますか?

_void President::setDefaultName()
{
   std::string local = "SO";
   name = local; // std::move OR not std::move?
}
_

私にとって、このローカル変数は期限切れの変数です-したがって、移動のセマンティクスを適用できます...これは、値で渡される引数に似ています...

ここで、オプティマイザが余分なlocal ALTOGETHERを削除するようにしたいと思います。残念ながら、実際にはそうではありません。 heapメモリが再生されると、コンパイラの最適化が難しくなります。参照 BoostCon 2013 Keynote:Chandler Carruth:Optimizing the Emergent Structures of C++ 。チャンドラーの話から私の気を引いたことの1つは、ヒープに割り当てられたメモリに関しては、オプティマイザは単純に諦める傾向があるということです。

残念な例については、以下のコードを参照してください。この例では_std::string_を使用していません。インラインアセンブリコードを使用して大幅に最適化されたクラスであり、直感に反するコードが生成されることが多いためです。侮辱に傷害を加えるために、_std::string_はおおまかに言ってgcc 4.7.2で参照カウント共有ポインターを大まかに言っています( copy-on-write最適化 、_std::string_)。したがって、_std::string_のないサンプルコード:

_#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   int len = sizeof literal;
   char* buffer = new char[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
   delete[] buffer;
}
_

明らかに、「as-if」ルールに従って、生成されたコードはこれに最適化できます

_int main() {
   std::printf("string literal\n");
}
_

リンク時最適化を有効にして(LTO)GCC 4.9.0とClang 3.5で試してみましたが、どれもコードをこのレベルに最適化できませんでした。生成されたアセンブリコードを確認しました。どちらもヒープにメモリを割り当て、コピーを行いました。まあ、ええ、それは残念です。

スタック割り当てられたメモリは異なります:

_#include <algorithm>
#include <cstdio>

int main() {
   char literal[] = { "string literal" };
   const int len = sizeof literal;
   char buffer[len];
   std::copy(literal, literal+len, buffer);
   std::printf("%s\n", buffer);
}
_

アセンブリコードを確認しました。ここでは、コンパイラはコードをbasicallystd::printf("string literal\n");に削減できます。

したがって、サンプルコードの余分なlocalを排除できるという私の期待は完全にはサポートされていません。スタックが割り当てられた配列を使用した後者の例が示すように、それを行うことができます。

C++ 03で記述された何百ものKLOCプロジェクトを想像してみてください-この_std::move_のどこにでも追加しますか?
[...]
しかし、私にはわかりません:値による受け渡しは、むしろ普遍的な参照ではありませんか?

"速度が必要ですか?計測"(- ハワード・ヒナント

最適化を行ってコードが遅くなったことを知るだけで、最適化を行っている状況に簡単に気づくでしょう。 :(私のアドバイスはハワード・ヒナントの測定と同じです。

_std::string getName()
{
   std::string local = "Hello SO!";
   return local; // std::move(local) is not needed nor probably correct
}
_

はい。ただし、この特殊なケースにはルールがあります。これは、名前付き戻り値の最適化(NRVO)と呼ばれます。

18
Ali

DR1579 によって修正された現在のルールは、NRVOableローカルまたはパラメーター、またはid-expressionローカル変数またはパラメーターを参照することは、returnステートメントの引数です。

これが機能するのは、returnステートメントの後に変数を再び使用できないためです。

それがそうでないことを除いて:

struct S {
    std::string s;
    S(std::string &&s) : s(std::move(s)) { throw std::runtime_error("oops"); }
};

S foo() {
   std::string local = "Hello SO!";
   try {
       return local;
   } catch(std::exception &) {
       assert(local.empty());
       throw;
   }
}

したがって、returnステートメントの場合でも、そのステートメントに現れるローカル変数またはパラメーターがその最後の使用であることは実際には保証されません変数。

ローカル変数の「最後の」使用がxvalue変換の対象になることを指定するために標準を変更できることは完全に問題外ではありません。問題は、「最後の」使用法が何であるかを定義することです。もう1つの問題は、これが関数内で非局所的な影響を与えることです。追加する下位のデバッグステートメントは、依存していたxvalue変換が発生しなくなったことを意味します。単一のステートメントは複数回実行される可能性があるため、単一のオカレンスルールでも機能しません。

Std-proposalsメーリングリストでの議論のための提案を提出することに興味がありますか?

14
ecatmur

私の質問:このstd :: moveは本当に必要ですか?私のポイントは、このp_nameがコンストラクターの本体で使用されていないということです。おそらく、言語にデフォルトで移動セマンティクスを使用するいくつかのルールがありますか?

もちろん必要です。 p_nameは左辺値なので、std::moveは、右辺値に変換して移動コンストラクタを選択するために必要です。

それは言語が言うことだけではありません-タイプが次のような場合はどうでしょうか:

struct Foo {
    Foo() { cout << "ctor"; }
    Foo(const Foo &) { cout << "copy ctor"; }
    Foo(Foo &&) { cout << "move ctor"; }
};

言語ではcopy ctor移動を省略した場合は印刷する必要があります。ここにはオプションはありません。コンパイラーはこれを別の方法で行うことはできません。

はい、コピー省略が引き続き適用されます。しかし、あなたの場合(初期化リスト)ではありません。コメントを参照してください。


それともあなたの質問はなぜそのパターンを使用しているのですか?

答えは、移動から利益を得て、引数の組み合わせの爆発を避けながら、渡された引数のコピーを保存したいときに安全なパターンを提供することです。

2つの文字列(つまり、コピーする2つの「重い」オブジェクト)を保持するこのクラスを考えます。

struct Foo {
     Foo(string s1, string s2)
         : m_s1{s1}, m_s2{s2} {}
private:
     string m_s1, m_s2;
};

それでは、さまざまなシナリオで何が起こるか見てみましょう。

テイク1

string s1, s2; 
Foo f{s1, s2}; // 2 copies for passing by value + 2 copies in the ctor

ああ、これは悪いです。ここでは、実際に必要なのは2つだけで、4つのコピーが発生します。 C++ 03では、Foo()引数をconst-refsにすぐに変換します。

テイク2

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}

今私たちは持っています

Foo f{s1, s2}; // 2 copies in the ctor

それははるかに良いです!

しかし、動きはどうですか?たとえば、一時的なものから:

string function();
Foo f{function(), function()}; // 2 moves + still 2 copies in the ctor

または、明示的にlvalueをctorに移動する場合:

Foo f{std::move(s1), std::move(s2)}; // 2 moves + still 2 copies in the ctor

それは良いことではありません。stringのムーブトラクターを使用して、Fooメンバーを直接初期化することもできます。

テイク3

したがって、Fooのコンストラクターにいくつかのオーバーロードを導入できます。

Foo(const string &s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, const string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(const string &s1, string &s2) : m_s1{s1}, m_s2{s2} {}
Foo(string &&s1, string &&s2) : m_s1{s1}, m_s2{s2} {}

だから、わかりました、今私たちは持っています

Foo f{function(), function()}; // 2 moves
Foo f2{s1, function()}; // 1 copy + 1 move

良い。しかし、一体、組み合わせ爆発が発生します:ありとあらゆる引数がconst-ref + rvalueバリアントに現れる必要があります。 4つの文字列を取得した場合はどうなりますか? 16人の俳優を書くつもりですか?

テイク4(良い方)

代わりに見てみましょう:

Foo(string s1, string s2) : m_s1{std::move(s1)}, m_s2{std::move(s2)} {}

このバージョンでは:

Foo foo{s1, s2}; // 2 copies + 2 moves
Foo foo2{function(), function()}; // 2 moves in the arguments + 2 moves in the ctor
Foo foo3{std::move(s1), s2}; // 1 copy, 1 move, 2 moves

動きは非常に安いので、このパターンはそれらから完全に利益を得るおよび組み合わせ爆発を回避します。確かにずっと下に移動できます

そして、私は例外的な安全の表面を傷つけることすらしませんでした。


より一般的な説明の一部として、次のスニペットを考えてみましょう。関連するすべてのクラスが、値渡しによるsのコピーを作成します。

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {s};

// s won't be touched any more from here on
}

私があなたを正しく知っていれば、コンパイラが最後にsを実際に使用していないことを本当に望んでいます:

{
// some code ...
std::string s = "123";

AClass obj {s};
OtherClass obj2 {s};
Anotherclass obj3 {std::move(s)}; // bye bye s

// s won't be touched any more from here on. 
// hence nobody will notice s is effectively in a "dead" state!
}

なぜコンパイラできないがそうするのかを説明しましたが、私はあなたの主張を理解します。特定の観点からは理にかなっています-sを最後の使用よりも長く存続させることはナンセンスです。 C++ 2xを考えるための食べ物だと思います。

10
peppe

さらに調査を行い、ネット上の別のフォーラムに問い合わせました。

残念ながら、これはstd::moveが必要なのは、C++標準でそう言われているだけでなく、そうでなければ危険です。

((comp.std.c ++からKalle Olavi Niemitaloへのクレジット- ここに彼の答え ))

#include <memory>
#include <mutex>
std::mutex m;
int i;
void f1(std::shared_ptr<std::lock_guard<std::mutex> > p);
void f2()
{
    auto p = std::make_shared<std::lock_guard<std::mutex> >(m);
    ++i;
    f1(p);
    ++i;
}

F1(p)が自動的にf1(std :: move(p))に変更された場合、ミューテックスは2番目の++ iの前にすでにロック解除されます。ステートメント。

次の例の方が現実的です。

#include <cstdio>
#include <string>
void f1(std::string s) {}
int main()
{
    std::string s("hello");
    const char *p = s.c_str();
    f1(s);
    std::puts(p);
}

F1(s)が自動的にf1(std :: move(s))に変更された場合、f1が戻った後、ポインターpは無効になります。

1
PiotrNycz