编译器在移动和复制构造函数之间的选择

时间:2013-11-16 14:52:50

标签: c++ c++11 copy-constructor move-constructor

最小例子:

#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行的输入/输出。如果我使用B行,而是出于某种奇怪的原因我得到std::move。在所有版本中,输出不依赖于我的输入(i的值除外)。

我的问题:

  • 为什么版本B和C不同?
  • 为什么编译器会在案例A和C中复制?

4 个答案:

答案 0 :(得分:4)

惊喜在哪里?您正在返回本地对象,但您没有直接返回它们。如果你直接返回一个局部变量,你将得到移动构造:

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

我认为,相关条款是12.8 [class.copy]第32段:

  

当满足或将满足复制操作的省略标准时,除了源对象是函数参数,并且要复制的对象由左值指定,重载决策以选择构造函数首先执行复制,就好像对象是由右值指定的一样。 [...]

但是,选择要从条件运算符中选择的命名对象不符合复制省略条件:编译器在构造对象之后才能知道要返回哪些对象并且复制省略是基于构造容易在它需要去的地方。

如果您有条件运算符,则有两种基本情况:

  1. 两个分支都生成完全相同的类型,结果将是对结果的引用。
  2. 分支以某种方式不同,结果将是从所选分支构建的临时结构。
  3. 也就是说,当返回c == 1? m1: m2时,你得到一个my_class&这是一个左值,因此,它被复制以产生返回值。您可能希望使用std::move(c == 1? m1: m2)来移动选定的局部变量。

    当您使用c == 1? std::move(m1): m2c == 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的值。在这两种情况下,条件表达式的结果都适用于复制省略,并且用于构建实际结果的复制/移动可能会被省略。

答案 1 :(得分:2)

条件操作员效应!

您正在通过条件运算符返回

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

第二个和第三个操作数具有相同的类型;结果是那种类型。如果操作数具有类类型,则结果是结果类型的prvalue临时值,它是第二个操作数或第三个操作数的 copy-initialized ,具体取决于第一个操作数的值。 [§5.16/ 6]

然后你有一份副本。此代码具有您期望的结果。

if (c==1)
   return m1;
else
   return m2;

答案 2 :(得分:1)

  1. 如果my_class的复制费用与复制int一样昂贵,那么。{ 编译器没有动机消除副本,事实上,它是 有动力做副本。不要忘记你的get(int c)功能 可以完全内联! It can lead to a very confusing output.您需要激励编译器尽力而为 通过为您的班级添加大而重的有效载荷来消除副本 这很复杂。

  2. 此外,请尝试编写,而不是依赖于未定义的行为 代码以明确定义的方式告诉您是移动还是移动 复制是否发生。

  3. 当您应用move时,还有2个有趣的案例:( i ) 当你使用三元条件运算符和( ii )的两个参数时 通过if - else而不是条件运算符返回。


  4. 我重新安排了你的代码:我给了my_class一个很重的有效载荷,复制真的很贵;我添加了一个成员函数,它以明确定义的方式告诉你是否复制了类;我添加了另外两个有趣的案例。

    #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
    

    至于发生了什么以及为什么,其他人已经在我写这个答案时回答了这个问题。如果您通过条件运算符,则会失去复制elision的资格。如果你申请move,你仍然可以逃脱移动建设。如果你看一下输出,那就是发生了什么。我在优化级别-O3使用clang 3.4 trunk和gcc 4.7.2测试了它;获得相同的输出。

答案 3 :(得分:-1)

编译器无需移动,移动点比复制和销毁要快得多。但两者产生相同的结果。