【问题标题】:Compiler's choice between move and copy constructor编译器在移动和复制构造函数之间的选择
【发布时间】:2013-11-16 14:52:50
【问题描述】:

小例子:

#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 中进行复制?

【问题讨论】:

  • @AlecTeal 你确定吗?我不期望复制省略,我的问题与省略构造函数/RVO 无关(编译器不能在这里做 RVO)。
  • 不,但它会教你如何构造和东西,如果你有 "T t; t=otherT;"如果您说“T t = otherT;”,它将使用分配即使您写了“=”,它也不会默认构造和分配。如果它是一个 r 值,它会移动。
  • 我不太明白您为什么会感到困惑,这种行为怎么会出乎意料?
  • 顺便说一句,刚刚针对 Clang 3.4 和 G++ 4.8.2 测试了您的代码,结果相同

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


【解决方案1】:

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

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

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

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

但是,从条件运算符中选择要选择的命名对象不符合复制省略的条件:编译器在构造对象之前无法知道要返回哪些对象,并且复制省略基于构造物体很容易放在它需要去的位置。

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

  1. 两个分支产生完全相同的类型,结果将是对结果的引用。
  2. 分支有所不同,结果将是从所选分支临时构建的。

也就是说,当返回c == 1? m1: m2 时,你会得到一个my_class&amp;,它是一个左值,因此被复制以产生返回值。您可能想使用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 的值。在这两种情况下,条件表达式的结果都符合复制省略的条件,用于构造实际结果的复制/移动可能会被省略。

【讨论】:

  • 谢谢,这解释了第二个问题。但首先,知道为什么版本 B 和 C 不同吗?
  • @Johannes:实际上,它不是很准确,但我会更新一下。 BC 不同的原因是临时返回的创建方式:它正在移动一个而不是另一个(但这并不像当前答案中应该的那样清楚)。
【解决方案2】:

条件运算符效果!

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

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

第二个和第三个操作数的类型相同;结果就是那种类型。如果操作数具有类类型,则结果是结果类型的临时纯右值,根据第一个操作数的值从第二个操作数或第三个操作数复制初始化。 [§ 5.16/6]

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

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

【讨论】:

  • 你说它是“取决于结果类型”?但是 B 和 C 中的结果类型是相同的。(用 decltype 尝试过,我在两个版本中都得到“8my_class”)。
  • @Johannes:不,正如引用所说,如果类型相同,那么结果将是复制初始化
  • 这如何解释第二个操作数上的std::move 与第三个操作数上的效果不同?
【解决方案3】:
  1. 如果复制my_class 与复制int 一样昂贵,则 编译器并没有消除副本的动机,事实上,它是 有动力去复制。不要忘记你的 get(int c) 函数 可以完全内联! It can lead to a very confusing output. 你需要激励编译器尽最大努力 通过向您的班级添加大而重的有效负载来消除副本 复制成本很高。

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

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


我重新安排了你的代码:我给了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

至于发生了什么以及为什么,在我写这个答案时,其他人已经回答了。如果您通过条件运算符,您将失去复制省略的资格。如果您申请move,您仍然可以使用移动构造。如果您查看输出,这正是发生的情况。我已经在优化级别-O3 下使用 clang 3.4 trunk 和 gcc 4.7.2 对其进行了测试;得到相同的输出。

【讨论】:

  • 谢谢,这个链接很有帮助。
  • @Johannes 很高兴听到这个消息。我希望这个答案仍然可以为 Dietmar 的答案增加一点价值。
【解决方案4】:

编译器不必移动,移动的目的是比复制和破坏要快得多。但两者产生相同的结果。

【讨论】:

  • 我认为复制也会涉及破坏。您是否将复制与 RVO 混淆了?
  • 编译器无法选择是移动复制还是复制。它只能选择是否删除副本。
  • @juanchopanza 我相信它确实如此,我相信这是移动的重点,以帮助我们避免复制大型结构的惩罚。你能引用标准吗?我知道它发布了一个关于什么构成 r 值的暂定列表,因为这个想法正在探索中
  • 重点是,与复制省略不同,是移动复制构造还是复制构造完全由标准规定。
  • @juanchopanza 我不相信你是对的,我知道规范定义了属性,如果它们被满足,它就是一个 R 值(它不是 IFF 关系)我不认为这是使用它们的标准。如果我错了,请纠正我!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-30
  • 2018-08-27
  • 1970-01-01
  • 1970-01-01
  • 2019-04-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多