3

最小限の例:

#include <iostream>

struct my_class
{
    int i;
    my_class() : i(0) { std::cout << "default" << std::endl; }
    my_class(const my_class&) { std::cout << "copy" << std::endl; }
    my_class(my_class&& other) { std::cout << "move" << std::endl; }
    my_class(const my_class&& other) { std::cout << "move" << std::endl; }
};

my_class get(int c)
{
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : m2; // A
    //return (c == 1) ? std::move(m1) : m2; // B
    //return (c == 1) ? m1 : std::move(m2); // C
}

int main()
{
    bool c;
    std::cin >> c;
    my_class m = get(c);
    std::cout << m.i << std::endl; // nvm about undefinedness
    return 0;
}

編集済み:

g++ -std=c++11 -Wall -O3 ctor.cpp -o ctor # g++ v 4.7.1

入力:

1

出力:

default
default
copy
-1220217339

これはライン A またはライン C の In/Output です。代わりにライン B を使用すると、std::move奇妙な理由が得られます。すべてのバージョンで、出力は入力に依存しません (i の値を除く)。

私の質問:

  • バージョン B と C が異なるのはなぜですか?
  • A と C のケースでコンパイラがコピーを作成するのはなぜでしょうか?
4

4 に答える 4

4

意外性はどこに…?ローカル オブジェクトを返していますが、直接返していません。ローカル変数を直接返す場合は、move の構築が行われます。

my_class f() {
    my_class variable;
    return variable;
}

関連する条項は、12.8 [class.copy] パラグラフ 32 だと思います。

コピー操作の省略の基準が満たされているか、ソース オブジェクトが関数パラメーターであり、コピーされるオブジェクトが左辺値によって指定されているという事実を除いて満たされる場合、コピーのコンストラクターを選択するためのオーバーロードの解決は次のとおりです。オブジェクトが右辺値によって指定されたかのように最初に実行されます。[...]

ただし、条件演算子から選択する名前付きオブジェクトを選択することは、コピー省略の対象にはなりません。コンパイラは、オブジェクトが構築されるまで、どのオブジェクトを返すかを知ることができず、コピー省略は、オブジェクトを簡単に構築することに基づいています。行く必要がある場所。

条件演算子がある場合、次の 2 つの基本的な状況があります。

  1. どちらの分岐もまったく同じ型を生成し、結果は結果への参照になります。
  2. ブランチは何らかの形で異なり、結果は選択されたブランチから一時的に構築されます。

つまり、返すときに左辺値であるc == 1? m1: m2a を取得し、my_class&それをコピーして戻り値を生成します。std::move(c == 1? m1: m2)おそらく、選択したローカル変数を移動するために使用したいと思うでしょう。

c == 1? std::move(m1): m2またはc == 1? m1: std::move(m2)タイプが異なり、次の結果が得られる場合

return c == 1? my_class(std::move(m1)): my_class(m2);

また

return c == 1? my_class(m1): my_class(std::move(m2));

つまり、式がどのように定式化されるかに応じて、一時的なものは一方のブランチで構築されたコピーであり、もう一方のブランチで構築されたムーブです。どのブランチが選択されるかは、 の値に完全に依存しますc。どちらの場合も、条件式の結果はコピー省略の対象となり、実際の結果を構築するために使用されるコピー/移動は省略される可能性があります。

于 2013-11-16T15:34:56.520 に答える
2

条件演算子効果!

条件演算子を介して返されます

return (c == 1) ? m1 : m2;

2 番目と 3 番目のオペランドは同じ型です。結果はそのタイプです。オペランドにクラス型がある場合、結果は結果型の一時的な prvalue であり、最初のオペランドの値に応じて、2 番目のオペランドまたは 3 番目のオペランドのいずれかからコピー初期化されます。[§ 5.16/6]

次に、コピーがあります。このコードには、期待される結果があります。

if (c==1)
   return m1;
else
   return m2;
于 2013-11-16T15:22:24.330 に答える
1
  1. my_classをコピーするのと同じくらいコストがかかる場合int、コンパイラはコピーを排除しようとはしません。実際、コンパイラはコピーを実行しようとします。get(int c)関数を完全にインライン化できること を忘れないでください。非常に紛らわしい出力になる可能性があります。コピーにコストがかかる大きくて重いペイロードをクラスに追加して、コピーを排除するために最善を尽くすようにコンパイラーを動機付ける必要があります。

  2. さらに、未定義の動作に頼るのではなく、移動またはコピーが発生したかどうかを明確に示すコードを作成するようにしてください。

  3. さらに興味深いケースが 2 つあります。( imove )三項条件演算子の両方の引数に適用する場合と、( ii )条件演算子の代わりにif-を介して戻る場合です。else


私はあなたのコードを再編成my_classしました。クラスがコピーされたかどうかを明確に示すメンバー関数を追加しました。他に2つの興味深いケースを追加しました。

#include <iostream>
#include <string>
#include <vector>

class weight {
public:  
    weight() : v(1024, 0) { };
    weight(const weight& ) : v(1024, 1) { }
    weight(weight&& other) { v.swap(other.v); }
    weight& operator=(const weight& ) = delete;
    weight& operator=(weight&& ) = delete;
    bool has_been_copied() const { return v.at(0); }
private:
    std::vector<int> v;
};

struct my_class {
    weight w;
};

my_class A(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : m2;
}

my_class B(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? std::move(m1) : m2;
}

my_class C(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? m1 : std::move(m2);
}

my_class D(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    return (c == 1) ? std::move(m1) : std::move(m2);
}

my_class E(int c) {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
    my_class m1;
    my_class m2;
    if (c==1) 
      return m1;
    else
      return m2;
}

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

    if (argc==1) {
      return 1; 
    }

    int i = std::stoi(argv[1]);

    my_class a = A(i);
    std::cout << a.w.has_been_copied() << std::endl;

    my_class b = B(i);
    std::cout << b.w.has_been_copied() << std::endl;

    my_class c = C(i);
    std::cout << c.w.has_been_copied() << std::endl;

    my_class d = D(i);
    std::cout << d.w.has_been_copied() << std::endl;

    my_class e = E(i);
    std::cout << e.w.has_been_copied() << std::endl;
}

で出力./a.out 0

my_class A(int)
1
my_class B(int)
1
my_class C(int)
0
my_class D(int)
0
my_class E(int)
0

で出力./a.out 1

my_class A(int)
1
my_class B(int)
0
my_class C(int)
1
my_class D(int)
0
my_class E(int)
0

何が起こったのか、その理由については、私がこの回答を書いているときに、他の人がすでに回答しています。条件演算子を通過すると、コピー省略の資格が失われます。申請すれば引っ越し工事を免れることもできますmove。出力を見ると、まさにそれが起こっています。最適化レベルで、clang 3.4 トランクと gcc 4.7.2 の両方でテストしました-O3。同じ出力が得られます。

于 2013-11-16T16:32:56.460 に答える
-1

コンパイラは移動する必要はありません。移動のポイントは、コピーと破棄よりもはるかに高速になることです。しかし、2つは同じ結果をもたらします。

于 2013-11-16T15:30:56.713 に答える