了解 copy/move 构造函数和运算符之间的推理
Understanding the reasoning between copy/move constructors and operators
我试图通过一个简单的自制示例来掌握右值引用和移动语义,但我无法理解特定部分。我创建了以下 class:
class A {
public:
A(int a) {
cout << "Def constructor" << endl;
}
A(const A& var) {
cout << "Copy constructor" << endl;
}
A(A&& var) {
cout << "Move constructor" << endl;
}
A& operator=(const A& var) {
cout << "Copy Assignment" << endl;
return *this;
}
A& operator=(A&& var) {
cout << "Move Assignment" << endl;
return *this;
}
};
我尝试了以下实验,看看是否可以预测 constructors/operators 将如何被调用:
- A a1(1) - 将调用默认构造函数。
预测。
- A a2 = a1 - 复制构造函数将被调用。 预测。
- a1 = a2 - 将调用复制赋值运算符。
预测。
现在,我创建了一个简单的函数,它只是 returns 一个 A 对象。
A helper() {
return A(1);
}
- A a3 = helper() - 默认构造函数将被调用
为了创建助手 returns 的对象。此举
由于 RVO,不会调用构造函数。 预测。
- a3 = helper() - 默认构造函数将被调用
为了创建助手 returns 的对象。然后,移动
赋值运算符将被调用。 预测。
现在是我不明白的部分。我创建了另一个完全没有意义的函数。它按值获取一个 A 对象,它只是 returns 它。
A helper_alt(A a) {
return a;
}
- A a4 = helper_alt(a1) - 这将调用复制构造函数,以
实际上复制函数中的对象a1然后移动
构造函数。 预测。
- a4 = helper_alt(a1) - 这将调用复制构造函数,以
实际上在函数中复制对象 a1 然后我认为
如我所见,移动赋值运算符将被称为 BUT,
首先,调用移动构造函数,然后调用移动赋值
运营商被称为。 不知道。
如果我说的有什么不对的地方,或者你觉得我可能有什么不明白的地方,请随时指正。
我的实际问题:在最后一个案例中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?
恭喜你,发现了一个C++核心问题!
关于您在示例代码中看到的行为,仍有很多讨论。
有如下建议:
A&& helper_alt(A a) {
std::cout << ".." << std::endl;
return std::move(a);
}
这会做你想做的,只需使用移动赋值,但会发出来自 g++ 的警告“警告:返回对局部变量 'a' 的引用”,即使该变量立即超出范围。
其他人已经发现了这个问题,这已经成为一个 c++ standard language core issue
有趣的是,这个问题在 2010 年就已经被发现,但直到现在才得到解决...
回答你的问题“在最后一个例子中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?" 是,C++ 委员会直到现在也没有答案。准确地说,有一个建议的解决方案,这个被接受,但直到现在还不是语言的一部分。
发件人:Comment Status
Amend paragraph 34 to explicitly exclude function parameters from copy elision. Amend paragraph 35 to include function parameters as eligible for move-construction.
考虑下面的例子。我使用 -fno-elide-constructors 标志编译示例代码以防止 RVO 优化:
g++ -fno-elide-constructors -o test test.cpp
#include<iostream>
using namespace std;
class A {
public:
A(int a) {
cout << "Def constructor" << endl;
}
A(const A& var) {
cout << "Copy constructor" << endl;
}
A(A&& var) {
cout << "Move constructor" << endl;
}
A& operator=(const A& var) {
cout << "Copy Assignment" << endl;
return *this;
}
A& operator=(A&& var) {
cout << "Move Assignment" << endl;
return *this;
}
};
A a_global(1);
A helper_alt(A a) {
return a;
}
A helper_a_local(A a) {
A x(1);
return x;
}
A helper_a_global(A a) {
return a_global;
}
int main(){
A a1(1);
A a4(4);
std::cout << "================= helper_alt(a1) ==================" << std::endl;
a4 = helper_alt(a1);
std::cout << "=============== helper_a_local() ================" << std::endl;
a4 = helper_a_local(a1);
std::cout << "=============== helper_a_global() ================" << std::endl;
a4 = helper_a_global(a1);
return 0;
}
这将导致以下输出:
Def constructor
Def constructor
Def constructor
================= helper_alt(a1) ==================
Copy constructor
Move constructor
Move Assignment
=============== helper_a_local() ================
Copy constructor
Def constructor
Move constructor
Move Assignment
=============== helper_a_global() ================
Copy constructor
Copy constructor
Move Assignment
简而言之,当 return 类型不是引用时,C++ 构造一个新的临时对象 (rvalue
),这会导致根据值类别和returned 对象的生命周期。
无论如何,我认为调用构造函数背后的逻辑是你没有使用引用,并且 returned 身份应该首先被解释,通过复制或移动构造函数,取决于 returned value category
或 return 对象的生命周期。再举个例子:
A helper_move_vs_copy(A a) {
// Call the Copy Constructor
A b = a;
// Call the Move Constructor, Due to the end of 'a' lifetime
return a;
}
int main(){
A a1(1);
A a2(4);
std::cout << "=============== helper_move_vs_copy() ================" << std::endl;
helper_move_vs_copy(a1);
return 0;
}
输出:
Def constructor
Def constructor
=============== helper_move_vs_copy() ================
Copy constructor
Copy constructor
Move constructor
来自cppreference:
an xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;
最后,RVO
的工作是通过优化代码来减少不必要的移动和复制,这甚至可以为基本程序员生成优化的二进制文件!
我试图通过一个简单的自制示例来掌握右值引用和移动语义,但我无法理解特定部分。我创建了以下 class:
class A {
public:
A(int a) {
cout << "Def constructor" << endl;
}
A(const A& var) {
cout << "Copy constructor" << endl;
}
A(A&& var) {
cout << "Move constructor" << endl;
}
A& operator=(const A& var) {
cout << "Copy Assignment" << endl;
return *this;
}
A& operator=(A&& var) {
cout << "Move Assignment" << endl;
return *this;
}
};
我尝试了以下实验,看看是否可以预测 constructors/operators 将如何被调用:
- A a1(1) - 将调用默认构造函数。 预测。
- A a2 = a1 - 复制构造函数将被调用。 预测。
- a1 = a2 - 将调用复制赋值运算符。 预测。
现在,我创建了一个简单的函数,它只是 returns 一个 A 对象。
A helper() {
return A(1);
}
- A a3 = helper() - 默认构造函数将被调用 为了创建助手 returns 的对象。此举 由于 RVO,不会调用构造函数。 预测。
- a3 = helper() - 默认构造函数将被调用 为了创建助手 returns 的对象。然后,移动 赋值运算符将被调用。 预测。
现在是我不明白的部分。我创建了另一个完全没有意义的函数。它按值获取一个 A 对象,它只是 returns 它。
A helper_alt(A a) {
return a;
}
- A a4 = helper_alt(a1) - 这将调用复制构造函数,以 实际上复制函数中的对象a1然后移动 构造函数。 预测。
- a4 = helper_alt(a1) - 这将调用复制构造函数,以 实际上在函数中复制对象 a1 然后我认为 如我所见,移动赋值运算符将被称为 BUT, 首先,调用移动构造函数,然后调用移动赋值 运营商被称为。 不知道。
如果我说的有什么不对的地方,或者你觉得我可能有什么不明白的地方,请随时指正。
我的实际问题:在最后一个案例中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?
恭喜你,发现了一个C++核心问题!
关于您在示例代码中看到的行为,仍有很多讨论。
有如下建议:
A&& helper_alt(A a) {
std::cout << ".." << std::endl;
return std::move(a);
}
这会做你想做的,只需使用移动赋值,但会发出来自 g++ 的警告“警告:返回对局部变量 'a' 的引用”,即使该变量立即超出范围。
其他人已经发现了这个问题,这已经成为一个 c++ standard language core issue
有趣的是,这个问题在 2010 年就已经被发现,但直到现在才得到解决...
回答你的问题“在最后一个例子中,为什么先调用移动构造函数然后调用移动赋值运算符,而不是仅仅调用移动赋值运算符?" 是,C++ 委员会直到现在也没有答案。准确地说,有一个建议的解决方案,这个被接受,但直到现在还不是语言的一部分。
发件人:Comment Status
Amend paragraph 34 to explicitly exclude function parameters from copy elision. Amend paragraph 35 to include function parameters as eligible for move-construction.
考虑下面的例子。我使用 -fno-elide-constructors 标志编译示例代码以防止 RVO 优化:
g++ -fno-elide-constructors -o test test.cpp
#include<iostream>
using namespace std;
class A {
public:
A(int a) {
cout << "Def constructor" << endl;
}
A(const A& var) {
cout << "Copy constructor" << endl;
}
A(A&& var) {
cout << "Move constructor" << endl;
}
A& operator=(const A& var) {
cout << "Copy Assignment" << endl;
return *this;
}
A& operator=(A&& var) {
cout << "Move Assignment" << endl;
return *this;
}
};
A a_global(1);
A helper_alt(A a) {
return a;
}
A helper_a_local(A a) {
A x(1);
return x;
}
A helper_a_global(A a) {
return a_global;
}
int main(){
A a1(1);
A a4(4);
std::cout << "================= helper_alt(a1) ==================" << std::endl;
a4 = helper_alt(a1);
std::cout << "=============== helper_a_local() ================" << std::endl;
a4 = helper_a_local(a1);
std::cout << "=============== helper_a_global() ================" << std::endl;
a4 = helper_a_global(a1);
return 0;
}
这将导致以下输出:
Def constructor
Def constructor
Def constructor
================= helper_alt(a1) ==================
Copy constructor
Move constructor
Move Assignment
=============== helper_a_local() ================
Copy constructor
Def constructor
Move constructor
Move Assignment
=============== helper_a_global() ================
Copy constructor
Copy constructor
Move Assignment
简而言之,当 return 类型不是引用时,C++ 构造一个新的临时对象 (rvalue
),这会导致根据值类别和returned 对象的生命周期。
无论如何,我认为调用构造函数背后的逻辑是你没有使用引用,并且 returned 身份应该首先被解释,通过复制或移动构造函数,取决于 returned value category
或 return 对象的生命周期。再举个例子:
A helper_move_vs_copy(A a) {
// Call the Copy Constructor
A b = a;
// Call the Move Constructor, Due to the end of 'a' lifetime
return a;
}
int main(){
A a1(1);
A a2(4);
std::cout << "=============== helper_move_vs_copy() ================" << std::endl;
helper_move_vs_copy(a1);
return 0;
}
输出:
Def constructor
Def constructor
=============== helper_move_vs_copy() ================
Copy constructor
Copy constructor
Move constructor
来自cppreference:
an xvalue (an “eXpiring” value) is a glvalue that denotes an object whose resources can be reused;
最后,RVO
的工作是通过优化代码来减少不必要的移动和复制,这甚至可以为基本程序员生成优化的二进制文件!