29

コンストラクターに最適な形式を考えています。サンプルコードは次のとおりです。

class Y { ... }

class X
{
public:
  X(const Y& y) : m_y(y) {} // (a)
  X(Y y) : m_y(y) {} // (b)
  X(Y&& y) : m_y(std::forward<Y>(y)) {} // (c)

  Y m_y;
}

Y f() { return ... }

int main()
{
  Y y = f();
  X x1(y); // (1)
  X x2(f()); // (2)
}

私が理解していることから、これはコンパイラーがそれぞれの状況で実行できる最善の方法です。

(1a)yはx1.m_yにコピーされます(1コピー)

(1b)yはXのコンストラクターの引数にコピーされ、次にx1.m_yにコピーされます(2コピー)

(1c)yはx1.m_yに移動されます(1移動)

(2a)f()の結果がx2.m_yにコピーされます(1コピー)

(2b)f()はコンストラクターの引数に組み込まれ、x2.m_yにコピーされます(1コピー)

(2c)f()がスタック上に作成され、x2.m_yに移動されます(1移動)

ここでいくつかの質問:

  1. どちらの場合も、const参照による受け渡しは悪くはなく、値による受け渡しよりも優れている場合があります。これは、 「スピードが欲しいですか?値渡し」に関する議論に反しているようです。。C ++(C ++ 0xではない)の場合、これらのコンストラクターのconst参照による受け渡しを使用する必要がありますか、それとも値による受け渡しを行う必要がありますか?また、C ++ 0xの場合、値渡しよりも右辺値参照を渡す必要がありますか?

  2. (2)の場合、一時的なものがx.m_yに直接構築されているとよいでしょう。右辺値バージョンでさえ、オブジェクトが動的メモリを割り当てない限り、コピーと同じくらいの作業である移動が必要だと思います。コンパイラがこれらのコピーや移動を回避できるようにこれをコーディングする方法はありますか?

  3. 私は、コンパイラーが最善を尽くすことができると思うことと、私の質問自体の両方で多くの仮定をしました。正しくない場合は、これらのいずれかを修正してください。

4

1 に答える 1

42

私はいくつかの例をまとめました。このすべてでGCC4.4.4を使用しました。

シンプルなケース、なし-std=c++0x

まず、それぞれを受け入れる2つのクラスを使用した非常に単純な例をまとめましたstd::string

#include <string>
#include <iostream>

struct A /* construct by reference */
  {
    std::string s_;

    A (std::string const &s) : s_ (s)
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    std::string s_;

    B (std::string s) : s_ (s)
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

static A f () { return A ("string"); }
static A f2 () { A a ("string"); a.s_ = "abc"; return a; }
static B g () { return B ("string"); }
static B g2 () { B b ("string"); b.s_ = "abc"; return b; }

int main ()
  {
    A a (f ());
    A a2 (f2 ());
    B b (g ());
    B b2 (g2 ());

    return 0;
  }

そのプログラムの出力はstdout次のとおりです。

A::<constructor>
A::<constructor>
B::<constructor>
B::<constructor>
B::<destructor>
B::<destructor>
A::<destructor>
A::<destructor>

結論

AGCCは、一時的またはB離れた場所ですべてを最適化することができました。これは、 C++FAQ と一致しています。基本的に、GCCは、値によって表示される関数が呼び出された場合でも、そのa, a2, b, b2 で構築するコードを生成する可能性があります(そして生成する意思があります) 。これにより、GCCは、コードを調べることで存在が「推測」された可能性のある一時的なものの多くを回避できます。

次に確認したいstd::stringのは、上記の例で実際にコピーされる頻度です。std::stringよりよく観察して見ることができるものに置き換えましょう。

現実的なケース、なし-std=c++0x

#include <string>
#include <iostream>

struct S
  {
    std::string s_;

    S (std::string const &s) : s_ (s)
      {
        std::cout << "  S::<constructor>" << std::endl;
      }
    S (S const &s) : s_ (s.s_)
      {
        std::cout << "  S::<copy constructor>" << std::endl;
      }
    ~S ()
      {
        std::cout << "  S::<destructor>" << std::endl;
      }
  };

struct A /* construct by reference */
  {
    S s_;

    A (S const &s) : s_ (s) /* expecting one copy here */
      {
        std::cout << "A::<constructor>" << std::endl;
      }
    A (A const &a) : s_ (a.s_)
      {
        std::cout << "A::<copy constructor>" << std::endl;
      }
    ~A ()
      {
        std::cout << "A::<destructor>" << std::endl;
      }
  };

struct B /* construct by value */
  {
    S s_;

    B (S s) : s_ (s) /* expecting two copies here */
      {
        std::cout << "B::<constructor>" << std::endl;
      }
    B (B const &b) : s_ (b.s_)
      {
        std::cout << "B::<copy constructor>" << std::endl;
      }
    ~B ()
      {
        std::cout << "B::<destructor>" << std::endl;
      }
  };

/* expecting a total of one copy of S here */
static A f () { S s ("string"); return A (s); }

/* expecting a total of one copy of S here */
static A f2 () { S s ("string"); s.s_ = "abc"; A a (s); a.s_.s_ = "a"; return a; }

/* expecting a total of two copies of S here */
static B g () { S s ("string"); return B (s); }

/* expecting a total of two copies of S here */
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); b.s_.s_ = "b"; return b; }

int main ()
  {
    A a (f ());
    std::cout << "" << std::endl;
    A a2 (f2 ());
    std::cout << "" << std::endl;
    B b (g ());
    std::cout << "" << std::endl;
    B b2 (g2 ());
    std::cout << "" << std::endl;

    return 0;
  }

そして、残念ながら、出力は期待を満たしています。

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
  S::<copy constructor>
B::<constructor>
  S::<destructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

結論

GCCは、のコンストラクターによって作成された一時的なものを最適化できませんでした。のデフォルトのコピーコンストラクタを使用しても、それは変わりませんでした。に変わるSBSf, g

static A f () { return A (S ("string")); } // still one copy
static B g () { return B (S ("string")); } // reduced to one copy!

示された効果がありました。GCCは、コンストラクターへの引数を適切に構築することをいとわないようですが、メンバーをB適切に構築することを躊躇しています。Bまだ一時的AまたはB作成されていないことに注意してください。つまりa, a2, b, b2、まだ適切に構築さているということです。涼しい。

ここで、新しい移動セマンティクスが2番目の例にどのように影響するかを調べてみましょう。

現実的なケース、-std=c++0x

次のコンストラクターをに追加することを検討してくださいS

    S (S &&s) : s_ ()
      {
        std::swap (s_, s.s_);
        std::cout << "  S::<move constructor>" << std::endl;
      }

そして、Bのコンストラクタをに変更します

    B (S &&s) : s_ (std::move (s)) /* how many copies?? */
      {
        std::cout << "B::<constructor>" << std::endl;
      }

この出力を取得します

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<copy constructor>
A::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

  S::<constructor>
  S::<move constructor>
B::<constructor>
  S::<destructor>

B::<destructor>
  S::<destructor>
B::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>
A::<destructor>
  S::<destructor>

したがって、パスバイ右辺値を使用して、 4つのコピー2つの移動に置き換えることができました。

しかし、実際には壊れたプログラムを作成しました。

想起g, g2

static B g ()  { S s ("string"); return B (s); }
static B g2 () { S s ("string"); s.s_ = "abc"; B b (s); /* s is zombie now */ b.s_.s_ = "b"; return b; }

マークされた場所は問題を示しています。一時的ではないオブジェクトに対して移動が行われました。これは、右辺値参照が一時値にもバインドされる可能性があることを除いて、左辺値参照のように動作するためです。Bしたがって、定数の左辺値参照をとるコンストラクターでのコンストラクターをオーバーロードすることを忘れてはなりません。

    B (S const &s) : s_ (s)
      {
        std::cout << "B::<constructor2>" << std::endl;
      }

次に、どちらの場合もシンボルが右辺値参照よりも定数参照に適しているため、両方g, g2が「constructor2」を呼び出すことに気付くでしょう。次の2つの方法のいずれかでs、コンパイラに移動を実行するように説得できます。g

static B g ()  { return B (S ("string")); }
static B g ()  { S s ("string"); return B (std::move (s)); }

結論

値による戻りを行います。このコードは、「私が提供する参照を埋める」コードよりも読みやすく、より高速で、おそらく例外安全性がさらに高くなります。

fに変更することを検討してください

static void f (A &result) { A tmp; /* ... */ result = tmp; } /* or */
static void f (A &result) { /* ... */ result = A (S ("string")); }

それは、の割り当てがそれを提供する場合にのみ、強力な保証を満たします。Aにコピーするresultことはスキップできません。また、が構築されていないためtmp、の代わりに構築することもできません。したがって、コピーが不要だった以前よりも遅くなります。C ++ 0xコンパイラとムーブ代入演算子はオーバーヘッドを削減しますが、それでも値による戻りよりも低速です。resultresult

値によるリターンは、より簡単に強力な保証を提供します。オブジェクトは所定の位置に構築されます。その一部が失敗し、他の部分がすでに構築されている場合、通常の巻き戻しはクリーンアップされ、Sコンストラクターが自身のメンバーに関する基本保証とグローバルアイテムに関する強力な保証を満たしている限り、全体が返されます。値によるプロセスは、実際には強力な保証を提供します。

とにかく(スタックに)コピーする場合は、常に値を渡します

スピードが欲しいで説明したように?値渡し。。コンパイラーは、可能であれば、呼び出し元の引数を適切に構成するコードを生成し、コピーを削除します。これは、参照して手動でコピーする場合には実行できません。主な例:これを書かないでください(引用された記事から引用)

T& T::operator=(T const& x) // x is a reference to the source
{ 
    T tmp(x);          // copy construction of tmp does the hard work
    swap(*this, tmp);  // trade our resources for tmp's
    return *this;      // our (old) resources get destroyed with tmp 
}

しかし、常にこれを好む

T& T::operator=(T x)    // x is a copy of the source; hard work already done
{
    swap(*this, x);  // trade our resources for x's
    return *this;    // our (old) resources get destroyed with x
}

スタック以外のフレームの場所にコピーする場合は、C ++ 0xより前のconst参照を渡し、さらにC++0xより後の右辺値参照を渡します。

私たちはすでにこれを見ました。参照渡しは、値渡しよりもインプレース構築が不可能な場合に発生するコピーが少なくなります。また、C ++ 0xの移動セマンティクスは、多くのコピーをより少なく安価な移動に置き換える可能性があります。ただし、移動すると、移動元のオブジェクトからゾンビが作成されることに注意してください。移動はコピーではありません。上に示したように、右辺値参照を受け入れるコンストラクターを提供するだけで問題が発生する可能性があります。

スタック以外のフレームの場所にコピーして、を持っている場合はswap、とにかく値を渡すことを検討してください(C ++ 0xより前)

swap 安価なデフォルトの構造を使用している場合は、それをaと組み合わせると、コピーするよりも効率的です。Sのコンストラクタを次のように考えます

    S (std::string s) : s_ (/* is this cheap for your std::string? */)
      {
        s_.swap (s); /* then this may be faster than copying */
        std::cout << "  S::<constructor>" << std::endl;
      }
于 2010-12-01T18:40:40.110 に答える