10

私は最近、Bjarne Stoustrup との講義に参加しました。彼は C++ 11 と、それが理にかなっている理由について話していました。

新しい素晴らしさの彼の例の 1 つは、ムーブ コンストラクターのニュース '&&' 記号でした。

それから私は家に帰りたいと思い始めました。

私の最初の例は以下のコードでした:

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const Number operator+(const Number& n0, const Number& n1); 
};

const Number operator+(const Number& n0, const Number& n1){
    return  Number(n0.value + n1.value);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1));
    cout << n3.toInt() << endl;
    return 0;
}

このコードは、移動コンストラクターが解決するはずのことを正確に実行します。n3 変数は、「+」演算子から返された値への参照から作成されます。

これがコードの実行からの出力であることを除いて:

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
3

RUN SUCCESSFUL 

出力が示すのは、コピー コンストラクターが呼び出されないことです。これは最適化がオフになっている場合です。コピー コンストラクターを実行できるように、コードのアームをひねるのに苦労しています。結果を std::pair でラップすることでうまくいきましたが、考え続けました。

演算子算術の移動コンストラクターの引数は、実際には失敗した引数ですか?

コピー コンストラクターが呼び出されないのはなぜですか? で呼び出されるのはなぜですか :

using namespace std;

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};

const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  make_pair(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

出力あり:

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL 

ロジックが何であるか、ペア演算子が基本的にパフォーマンスを台無しにする理由を知りたいですか?

アップデート:

別の変更を行ったところmake_pair、ペアの実際のテンプレート化されたコンストラクターに置き換えるとpair<const Number, const Number>、コピー コンストラクターが起動される回数が減ることがわかりました。

class Number {
private:
    int value;
public:
    Number(const int value) : value(value){
        cout << "Build Constructor on " << value << endl;
    }
    Number(const Number& orig) : value(orig.value){
        cout << "Copy Constructor on " << value << endl;        
    }
    virtual ~Number(){}

    int toInt() const{
        return value;
    }


    friend const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1); 
};



const std::pair<const Number, const Number> operator+(const Number& n0, const Number& n1){
    return  std::pair<const Number, const Number>(Number(n0.value + n1.value), n0);
}

int main(int argc, char** argv) {

    const Number n3 = (Number(2) + Number(1)).first;
    cout << n3.toInt() << endl;
    return 0;
}

出力:

Build Constructor on 1
Build Constructor on 2
Build Constructor on 3
Copy Constructor on 3
Copy Constructor on 2
Copy Constructor on 3
3

RUN SUCCESSFUL

それで、それmake_pairは有害であるように見えますか?

4

2 に答える 2

16

この単純なC++コードを考えてみましょう。

class StringHolder
{
  std::string member;
public:

  StringHolder(const std::string &newMember) : member(newMember) {}
};

std::string value = "I am a string that will probably be heap-allocated.";
StringHolder hold(value);

2行目を実行した後、文字列のコピーはいくつ存在しますか?答えは2つです。1つはに保存されvalue、もう1つはに保存されholdます。それは結構です...時々。自分のために文字列を保持しながら、誰かに文字列のコピーを渡したい場合がよくあります。しかし、あなたもそれをしたくない場合があります。例えば:

StringHolder hold("I am a string that will probably be heap-allocated.");

これによりstd::string一時的なものが作成され、それがStringHolderのコンストラクターに渡されます。コンストラクターはそのメンバーをコピー構築します。コンストラクターが完了すると、一時的なものは破棄されます。ある時点で、理由もなく、文字列のコピーが2つありました。

文字列のコピーを2つ持つ意味はありませんでした。私たちがやりたかったのは、パラメータをに移動して、文字列のコピーが1つだけになるようにすることでした。std::stringStringHolder

そこで、ムーブ構造が登場します。

Astd::stringは基本的に、割り当てられた文字配列へのポインターと、その配列の長さ(および容量ですが、今は気にしないでください)を含むサイズのラッパーにすぎません。があり、それを別の文字列std::string移動する場合は、新しい文字列が割り当てられた文字配列の所有権を主張し、古い文字列が所有権を放棄する必要があります。swapC ++ 03では、次の操作でこれを行うことができます。

std::string oldStr = "I am a string that will probably be heap-allocated.";
std::string newStr;
std::swap(newStr, oldStr);

oldStrこれにより、メモリが割り当てられnewStrずにの内容がに移動します。

C ++ 11の移動構文は、提供std::swapしない2つの重要な機能を提供します。

まず、移動は暗黙的に発生する可能性があります(ただし、安全に実行できる場合に限ります)。swapスワッピングが必要な場合は、明示的に呼び出す必要があります。移動は、自然なコードを書くことによって発生する可能性があります。たとえば、StringHolder前から私たちを取り、1つの変更を加えます:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}
};

StringHolder hold("I am a string that will probably be heap-allocated.");

この文字列のコピーはこれまでにいくつ作成されましたか?答えは...ただ1つです:一時的なものの建設。これは一時的なものであるため、C ++ 11は、それによって初期化されているものをすべて移動構築できることを知っているほど賢いです。したがって、コンストラクターの値パラメーターを移動して構築しますStringHolder(または、構築を完全に削除する可能性が高くなります)。これにより、保存されているメモリが一時メモリからに移動しnewMemberます。したがって、コピーは行われません。

その後、を構築するときに、moveコンストラクターを明示的に呼び出しmemberます。これにより、割り当てられたメモリがからnewMemberに移動しmemberます。

文字列を割り当てるのは1回だけです。これにより、パフォーマンスを大幅に節約できます。

さて、これはあなた自身の型のコンストラクターとどのように関係していますか?さて、このコードを考えてみましょう:

class StringHolder
{
  std::string member;
public:

  StringHolder(std::string newMember) : member(std::move(newMember)) {}

  StringHolder(const StringHolder &old) : member(old.member) {}
  StringHolder(StringHolder &&old) : member(std::move(old.member)) {}
};

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(oldHold);

今回は、コピーと移動のコンストラクターを持つクラスができました。文字列のコピーはいくつ取得できますか?

二。もちろん2つです。とがoldHoldありnewHold、それぞれに文字列のコピーがあります。

しかし、これを行った場合:

StringHolder oldHold = std::string("I am a string that will probably be heap-allocated.");
StringHolder newHold(std::move(oldHold));

そうすると、文字列のコピーが1つだけ存在することになります。

そのため、動きが重要です。それが重要な理由です:それはあなたが横になっている必要があるかもしれないもののコピーの数を減らします。


コピーコンストラクタが呼び出されないのはなぜですか

コピーコンストラクターは省略されたため、呼び出されませんでした。戻り値の最適化を行っています。ほとんどのコンパイラはとにかく削除されるため、最適化をオフにしても効果はありません。いつエリジオンが可能であるかを知らない理由はありません。

関数の戻り値の場合、省略できない場合は移動が重要です。

于 2012-05-20T11:36:32.323 に答える
2

これでオーバーロードする場合は、移動セマンティクスの価値を理解するのに役立つ場合がありますoperator+

Number operator+(Number&& n0, Number&& n1){
  n0.value += n1.value;
  return std::move(n0);
}

これには 2 つの重要な変更があります。

  • 非定数値を返します
  • 新しいオブジェクトを作成する代わりに、右辺値引数を受け入れ、それらの1つを変更します

これにより、例で「ビルドコンストラクター」呼び出しの1つを回避できます

Build Constructor on 1
Build Constructor on 2
Copy Constructor on 3
3

現在、コードは 3 つの新しいオブジェクトを作成するのではなく、2 つの新しいオブジェクトを作成して 1 つをコピーしているため、これまでのところ大きな利点はありません。ただし、コピーの代わりに使用できるムーブ コンストラクターを追加すると、次のようになります。

Number(Number&& orig) : value(orig.value){
  cout << "Move Constructor on " << value << endl;
}

Build Constructor on 1
Build Constructor on 2
Move Constructor on 3
3

クラスが「ビルド」コンストラクターと「コピー」コンストラクターでメモリを割り当てる場合、割り当ての総数が元のコードの 3 つから 2 つに減ります (ムーブ コンストラクターは何も割り当てず、割り当てられたメモリの所有権を取得すると仮定します)。移動元のオブジェクトが所有します。)

ここで、計算を次のように変更すると:

Number n3 = Number(2) + Number(1) + Number(0);

元のコードと移動可能なバージョンを比較すると、「割り当て」の数が 5 から 3 に減っていることがわかります。関係する一時オブジェクトが多いほど、新しいオブジェクトを作成する代わりに、一時オブジェクトを変更して移動する利点が大きくなります。利点は、コピーを回避するだけでなく、代わりに既存のオブジェクトからリソースの所有権を取得することで、新しいオブジェクトの新しいリソースを作成することを回避することです。

于 2012-05-20T23:13:17.347 に答える