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

Compiler's choice between move and copy constructor

本文关键字:之间 选择 构造函数 复制 移动 编译器      更新时间:2023-10-16

>最小示例:

#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 的情况下制作副本?

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

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

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

当满足或将满足复制操作省略的条件(源对象是函数参数,并且要复制的对象由左值指定)时,首先执行重载解析以选择复制的构造函数,就像对象由右值指定一样。[...]

但是,选择要从条件运算符中选择的命名对象不符合复制 elision 的条件:编译器直到构造对象后才能知道要返回哪些对象,并且复制 elision 基于在需要去的位置轻松构造对象。

当您有条件运算符时,有两种基本情况:

  1. 两个分支生成完全相同的类型,结果将是对结果的引用。
  2. 分支以某种方式不同,结果将从所选分支临时构造。

也就是说,当返回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的值。在这两种情况下,条件表达式的结果都符合复制省略条件,并且用于构造实际结果的复制/移动可能会被省略。

条件运算符效应!

您将通过条件运算符返回

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

第二个和第三个操作数具有相同的类型;结果属于该类型。如果操作数具有类类型,则结果是结果类型的临时 prvalue,根据第一个操作数的值,从第二个操作数或第三个操作数进行复制初始化。[§ 5.16/6]

然后你有一个副本。此代码具有预期的结果。

if (c==1)
   return m1;
else
   return m2;
  1. 如果复制my_class的成本与复制int一样昂贵,编译器并没有消除副本的动机,事实上,它是有动力做副本。不要忘记您的get(int c)功能可以完全内联! 它可能导致非常混乱输出。你需要激励编译器尽最大努力通过向类中添加大而重的有效负载来消除副本复制起来很昂贵。

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

  3. 还有两个更有趣的案例:(i)当您申请move三元条件运算符的参数和 (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,您仍然可以侥幸逃脱移动结构。如果你看一下输出,这正是发生的事情。我已经在优化级别 -O3 中使用 clang 3.4 主干和 gcc 4.7.2 对其进行了测试;获得相同的输出。

编译器不必

移动,移动的要点是比复制和破坏快得多。但两者产生相同的结果。