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

我的问题:

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

4 回答 4

4

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

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

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

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

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

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

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

于 2013-11-16T15:34:56.520 回答
2

条件运算符效果!

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

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

第二个和第三个操作数的类型相同;结果就是那种类型。如果操作数具有类类型,则结果是结果类型的临时纯右值,根据第一个操作数的值从第二个操作数或第三个操作数复制初始化。[§ 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 个更有趣的情况:(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。如果您查看输出,这正是发生的情况。我已经在优化级别使用 clang 3.4 trunk 和 gcc 4.7.2 对其进行了测试-O3;获得相同的输出。

于 2013-11-16T16:32:56.460 回答
-1

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

于 2013-11-16T15:30:56.713 回答