编译器在移动和复制构造函数之间的选择
Compiler's choice between move and copy 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 的情况下制作副本?
惊喜在哪里...?您正在返回本地对象,但不直接返回它们。如果你直接返回一个局部变量,你会得到移动构造:
my_class f() {
my_class variable;
return variable;
}
我认为,相关条款是12.8 [class.copy]第32段:
当满足或将满足复制操作省略的条件(源对象是函数参数,并且要复制的对象由左值指定)时,首先执行重载解析以选择复制的构造函数,就像对象由右值指定一样。[...]
但是,选择要从条件运算符中选择的命名对象不符合复制 elision 的条件:编译器直到构造对象后才能知道要返回哪些对象,并且复制 elision 基于在需要去的位置轻松构造对象。
当您有条件运算符时,有两种基本情况:
- 两个分支生成完全相同的类型,结果将是对结果的引用。
- 分支以某种方式不同,结果将从所选分支临时构造。
也就是说,当返回c == 1? m1: m2
时,你会得到一个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
的值。在这两种情况下,条件表达式的结果都符合复制省略条件,并且用于构造实际结果的复制/移动可能会被省略。
条件运算符效应!
您将通过条件运算符返回
return (c == 1) ? m1 : m2;
第二个和第三个操作数具有相同的类型;结果属于该类型。如果操作数具有类类型,则结果是结果类型的临时 prvalue,根据第一个操作数的值,从第二个操作数或第三个操作数进行复制初始化。[§ 5.16/6]
然后你有一个副本。此代码具有预期的结果。
if (c==1)
return m1;
else
return m2;
-
如果复制
my_class
的成本与复制int
一样昂贵,编译器并没有消除副本的动机,事实上,它是有动力做副本。不要忘记您的get(int c)
功能可以完全内联! 它可能导致非常混乱输出。你需要激励编译器尽最大努力通过向类中添加大而重的有效负载来消除副本复制起来很昂贵。 -
此外,与其依赖未定义的行为,不如尝试编写以明确定义的方式告诉您是移动还是复制是否发生。
-
还有两个更有趣的案例:(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 对其进行了测试;获得相同的输出。
移动,移动的要点是比复制和破坏快得多。但两者产生相同的结果。
- 如何在"push_*()"和"emplace_*()"函数之间进行选择?
- 如何在不同类型的值之间进行选择以传递给多态函数?
- 在C++同名的顶级函数之间进行选择
- 如何让CMake在多个编译器之间进行选择?
- 有没有办法根据模板参数的类型在不同的类实现之间进行选择
- 使用STD :: String和字符数组之间的选择
- 根据编译时条件在类型之间选择类型的惯用方法
- 如何根据定义的字符串类型在“std::cout”和“std::wcout”之间进行选择
- 有没有一种方法可以在基于枚举的可变参数模板函数之间进行选择,这比将函数包装在结构中更简单
- 为什么在具有相同签名的模板化和非模板化函数之间进行选择时没有歧义?
- C++ 在列表和列表之间选择返回类型<<string>std::p air<string,string>>
- C 编译器如何在延期和异步执行std :: async之间进行选择
- 如何在map和undered_map之间进行选择
- 一般来说,如何在C++中的结构和类之间进行选择
- 在一维和二维数组之间进行选择
- unix中选择和轮询系统调用之间的功能差异
- 在模板功能和自动类型扣除之间进行选择
- 在两个函数之间选择的函子
- C 编译器可以在用户定义和编译器生成的复制构建器之间进行自由选择
- 在映射或unordered_map之间选择由计算的双精度值组成的键