C++11 - 为什么编译器不优化 const 左值引用绑定的右值引用?
C++11 - Why compiler does not optimize rvalue reference to const lvalue reference binding?
在下一个测试代码中,我们有一个简单的 class MyClass
,只有一个变量成员 (int myValue
) 和一个函数 (MyClass getChild()
) return 是 MyClass
的新实例。此 class 的主要运算符在调用时重载以打印。
我们有三个函数,两个参数执行一个简单的赋值 (first_param = second_param): :
func1
:第二个参数是一个右值引用(也使用了一个std::forward
)
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
func2
: 第二个参数是一个const左值引用
void func2(MyClass &el, const MyClass &c) {
el = c;
}
func3
:两个重载(一个相当于func1
,另一个相当于func2
)
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
在main()
函数中我们分别调用这三个函数三次,一次传递一个rvalue,另一个传递一个lvalue,另一个传递 std::move(lvalue)
(或者相同的是,右值引用)。在调用这些函数之前,我们还对 lvalue、rvalue 和 进行直接赋值(不调用任何函数)右值引用.
测试代码:
#include <iostream>
#include <utility>
class MyClass {
public:
int myValue;
MyClass(int n) { // custom constructor
std::cout << "MyClass(int n) [custom constructor]" << std::endl;
}
MyClass() { // default constructor
std::cout << "MyClass() [default constructor]" << std::endl;
}
~MyClass() { // destructor
std::cout << "~MyClass() [destructor]" << std::endl;
}
MyClass(const MyClass& other) // copy constructor
: myValue(other.myValue)
{
std::cout << "MyClass(const MyClass& other) [copy constructor]" << std::endl;
}
MyClass(MyClass&& other) noexcept // move constructor
: myValue(other.myValue)
{
std::cout << "MyClass(MyClass&& other) [move constructor]" << std::endl;
}
MyClass& operator=(const MyClass& other) { // copy assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(const MyClass& other) [copy assignment]" << std::endl;
return *this;
}
MyClass& operator=(MyClass&& other) noexcept { // move assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(MyClass&& other) [move assignment]" << std::endl;
return *this;
}
MyClass getChild() const {
return MyClass(myValue+1);
}
};
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func2(MyClass &el, const MyClass &c) {
el = c;
}
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
int main(int argc, char** argv) {
MyClass root(200);
MyClass ch = root.getChild();
MyClass result;
std::cout << "==================================================================" << std::endl;
std::cout << "------------- simple assignment to rvalue ------------------------" << std::endl;
result = root.getChild();
std::cout << "------------- simple assignment to lvalue ------------------------" << std::endl;
result = ch;
std::cout << "------------- simple assignment to std::move(lvalue) -------------" << std::endl;
result = std::move(ch);
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func1 with rvalue ----------------------------------" << std::endl;
func1(result, root.getChild());
std::cout << "------------- func1 with lvalue ----------------------------------" << std::endl;
//func1(result, ch); // does not compile
std::cout << "** Compiler error **" << std::endl;
std::cout << "------------- func1 with std::move(lvalue) -----------------------" << std::endl;
func1(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func2 with rvalue ----------------------------------" << std::endl;
func2(result, root.getChild());
std::cout << "------------- func2 with lvalue ----------------------------------" << std::endl;
func2(result, ch);
std::cout << "------------- func2 with std::move(lvalue) -----------------------" << std::endl;
func2(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func3 with rvalue ----------------------------------" << std::endl;
func3(result, root.getChild());
std::cout << "------------- func3 with lvalue ----------------------------------" << std::endl;
func3(result, ch);
std::cout << "------------- func3 with std::move(lvalue) -----------------------" << std::endl;
func3(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
return 0;
}
用g++编译后(用-O0或-O3都无所谓)和运行结果是:
MyClass(int n) [custom constructor]
MyClass(int n) [custom constructor]
MyClass() [default constructor]
==================================================================
------------- simple assignment to rvalue ------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- simple assignment to lvalue ------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- simple assignment to std::move(lvalue) -------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func1 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func1 with lvalue ----------------------------------
** Compiler error **
------------- func1 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func2 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(const MyClass& other) [copy assignment]
~MyClass() [destructor]
------------- func2 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func2 with std::move(lvalue) -----------------------
MyClass& operator=(const MyClass& other) [copy assignment]
==================================================================
------------- func3 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func3 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func3 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
~MyClass() [destructor]
~MyClass() [destructor]
~MyClass() [destructor]
对于作业,结果符合预期。如果你传递一个右值,它调用移动赋值,如果你传递一个左值,它调用复制赋值,如果你传递一个右值引用 (std::move(lvalue)
) 它调用移动赋值。
对 func1
的调用也是预期的(请记住,此函数接收 右值引用 )。如果你传递一个rvalue,它调用移动赋值,如果你传递一个lvalue,编译失败(因为一个lvalue 无法绑定到 右值引用 ),如果您传递右值引用 (std::move(lvalue)
),它会调用移动赋值。
但是对于func2
,三种情况下,都调用了复制赋值。这个函数接收一个const lvalue reference作为第二个参数,这是一个lvalue,然后它调用复制赋值。我理解这一点,但是,为什么编译器在使用时间对象(rvalue 或 rvalue 引用)调用时不优化此函数调用移动赋值运算符而不是复制赋值?
func3
试图创建一个与直接赋值工作方式相同的函数,它结合了 func1
行为并定义了具有 func2
行为的重载传递了一个 lvalue。这可行,但此解决方案需要将函数代码复制到两个函数中(不完全是,因为在一个解决方案中我们必须使用 std::forward
)。有没有办法通过避免重复代码来实现这一点?此功能很小,但在其他情况下可能会更大。
总结起来有两个问题:
为什么 func2
函数在收到 rvalue 或 rvalue 引用 时未优化以调用移动赋值?
我如何修改 func3
函数才能避免“复制”代码?
编辑以澄清我在 之后的想法。
我明白第一点(为什么编译器不优化这一点)。它只是根据语言的定义如何工作,编译器不能简单地优化它,因为在每种情况下必须调用哪些运算符已经明确定义并且必须遵守。程序员期望某些运算符被调用,而优化尝试会不可预测地改变调用它们的方式和方式。我遇到的唯一例外是 Return 值优化 (RVO),其中编译器可以消除为保存函数的 return 值而创建的临时对象;以及可以应用 Copy Elision 来消除不必要的对象复制的情况。根据其wikipedia article,优化不能应用于已绑定到引用的临时对象(我认为这正是适用于我们的情况):
Another widely implemented optimization, described in the C++
standard, is when a temporary object of class type is copied to an
object of the same type. As a result, copy-initialization is usually
equivalent to direct-initialization in terms of performance, but not
in semantics; copy-initialization still requires an accessible copy
constructor. The optimization can not be applied to a temporary object
that has been bound to a reference.
关于避免重复代码,我尝试了建议的 SO 帖子 (, ) 中的解决方案,这在某些情况下可能很方便,但它们不能完全替代重复的解决方案代码 (func3
),因为当将 rvalue 或 rvalue reference 传递给函数时它们可以正常工作,但它们不会传递 lvalue.
时完全按预期工作
为了对此进行测试,考虑到原始代码,我们添加了两个函数 func4
和 func5
来实现建议的解决方案:
template<typename T>
inline constexpr void func4(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func4(T &el, const T &c) {
T copy = c;
func4(el, std::move(copy));
}
template<class T>
std::decay_t<T> copy(T&& t) {
return std::forward<T>(t);
}
template<typename T>
inline constexpr void func5(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func5(T &el, const T &c) {
func5(el, copy(c));
}
与原始函数一样,我们使用 rvalue、lvalue 和 rvalue 引用来调用这些函数(std::move(lvalue)
),结果如下:
==================================================================
------------- func4 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func5 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
在lvalue的情况下,不是直接调用拷贝赋值,而是调用拷贝构造函数创建一个临时对象,然后调用移动赋值运算符;这比只调用复制赋值运算符而不创建临时对象(这是 func3
通过复制代码所做的)效率更低。
据我了解,目前还没有完全等效的方法来避免代码重复。
想想下面的例子:
void foo(int) {}
void foo(double) {}
void bar(double x) {
foo(x);
}
int main() {
bar(0);
}
在上面的程序中,总是会调用foo(double)
,而不是foo(int)
。这是因为虽然参数最初是 int
,但一旦您进入 bar
,此信息就无关紧要了。 bar
只看到它自己的参数 x
,无论原始参数类型是什么,它的类型都是 double
。因此,它调用与参数类型 x
.
最匹配的 foo
的重载
您的 func2
工作方式类似:
void func2(MyClass &el, const MyClass &c) {
el = c;
}
此处,表达式 c
是一个左值,即使引用可能在调用时已绑定到临时对象。因此,编译器必须 select =
将左值作为其右参数的运算符。
为了将左值转发为左值,将右值转发为右值,经常使用 const MyClass&
和 MyClass&&
重载,即使(如您所见)它是重复的。有关如何减少代码重复的一些建议,请参阅 and
在下一个测试代码中,我们有一个简单的 class MyClass
,只有一个变量成员 (int myValue
) 和一个函数 (MyClass getChild()
) return 是 MyClass
的新实例。此 class 的主要运算符在调用时重载以打印。
我们有三个函数,两个参数执行一个简单的赋值 (first_param = second_param): :
func1
:第二个参数是一个右值引用(也使用了一个std::forward
)
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
func2
: 第二个参数是一个const左值引用
void func2(MyClass &el, const MyClass &c) {
el = c;
}
func3
:两个重载(一个相当于func1
,另一个相当于func2
)
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
在main()
函数中我们分别调用这三个函数三次,一次传递一个rvalue,另一个传递一个lvalue,另一个传递 std::move(lvalue)
(或者相同的是,右值引用)。在调用这些函数之前,我们还对 lvalue、rvalue 和 进行直接赋值(不调用任何函数)右值引用.
测试代码:
#include <iostream>
#include <utility>
class MyClass {
public:
int myValue;
MyClass(int n) { // custom constructor
std::cout << "MyClass(int n) [custom constructor]" << std::endl;
}
MyClass() { // default constructor
std::cout << "MyClass() [default constructor]" << std::endl;
}
~MyClass() { // destructor
std::cout << "~MyClass() [destructor]" << std::endl;
}
MyClass(const MyClass& other) // copy constructor
: myValue(other.myValue)
{
std::cout << "MyClass(const MyClass& other) [copy constructor]" << std::endl;
}
MyClass(MyClass&& other) noexcept // move constructor
: myValue(other.myValue)
{
std::cout << "MyClass(MyClass&& other) [move constructor]" << std::endl;
}
MyClass& operator=(const MyClass& other) { // copy assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(const MyClass& other) [copy assignment]" << std::endl;
return *this;
}
MyClass& operator=(MyClass&& other) noexcept { // move assignment
myValue = other.myValue;
std::cout << "MyClass& operator=(MyClass&& other) [move assignment]" << std::endl;
return *this;
}
MyClass getChild() const {
return MyClass(myValue+1);
}
};
void func1(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func2(MyClass &el, const MyClass &c) {
el = c;
}
void func3(MyClass &el, MyClass &&c) {
el = std::forward<MyClass>(c);
}
void func3(MyClass &el, const MyClass &c) {
el = c;
}
int main(int argc, char** argv) {
MyClass root(200);
MyClass ch = root.getChild();
MyClass result;
std::cout << "==================================================================" << std::endl;
std::cout << "------------- simple assignment to rvalue ------------------------" << std::endl;
result = root.getChild();
std::cout << "------------- simple assignment to lvalue ------------------------" << std::endl;
result = ch;
std::cout << "------------- simple assignment to std::move(lvalue) -------------" << std::endl;
result = std::move(ch);
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func1 with rvalue ----------------------------------" << std::endl;
func1(result, root.getChild());
std::cout << "------------- func1 with lvalue ----------------------------------" << std::endl;
//func1(result, ch); // does not compile
std::cout << "** Compiler error **" << std::endl;
std::cout << "------------- func1 with std::move(lvalue) -----------------------" << std::endl;
func1(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func2 with rvalue ----------------------------------" << std::endl;
func2(result, root.getChild());
std::cout << "------------- func2 with lvalue ----------------------------------" << std::endl;
func2(result, ch);
std::cout << "------------- func2 with std::move(lvalue) -----------------------" << std::endl;
func2(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
std::cout << "------------- func3 with rvalue ----------------------------------" << std::endl;
func3(result, root.getChild());
std::cout << "------------- func3 with lvalue ----------------------------------" << std::endl;
func3(result, ch);
std::cout << "------------- func3 with std::move(lvalue) -----------------------" << std::endl;
func3(result, std::move(ch));
std::cout << "==================================================================" << std::endl;
return 0;
}
用g++编译后(用-O0或-O3都无所谓)和运行结果是:
MyClass(int n) [custom constructor]
MyClass(int n) [custom constructor]
MyClass() [default constructor]
==================================================================
------------- simple assignment to rvalue ------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- simple assignment to lvalue ------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- simple assignment to std::move(lvalue) -------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func1 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func1 with lvalue ----------------------------------
** Compiler error **
------------- func1 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func2 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(const MyClass& other) [copy assignment]
~MyClass() [destructor]
------------- func2 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func2 with std::move(lvalue) -----------------------
MyClass& operator=(const MyClass& other) [copy assignment]
==================================================================
------------- func3 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func3 with lvalue ----------------------------------
MyClass& operator=(const MyClass& other) [copy assignment]
------------- func3 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
~MyClass() [destructor]
~MyClass() [destructor]
~MyClass() [destructor]
对于作业,结果符合预期。如果你传递一个右值,它调用移动赋值,如果你传递一个左值,它调用复制赋值,如果你传递一个右值引用 (std::move(lvalue)
) 它调用移动赋值。
对 func1
的调用也是预期的(请记住,此函数接收 右值引用 )。如果你传递一个rvalue,它调用移动赋值,如果你传递一个lvalue,编译失败(因为一个lvalue 无法绑定到 右值引用 ),如果您传递右值引用 (std::move(lvalue)
),它会调用移动赋值。
但是对于func2
,三种情况下,都调用了复制赋值。这个函数接收一个const lvalue reference作为第二个参数,这是一个lvalue,然后它调用复制赋值。我理解这一点,但是,为什么编译器在使用时间对象(rvalue 或 rvalue 引用)调用时不优化此函数调用移动赋值运算符而不是复制赋值?
func3
试图创建一个与直接赋值工作方式相同的函数,它结合了 func1
行为并定义了具有 func2
行为的重载传递了一个 lvalue。这可行,但此解决方案需要将函数代码复制到两个函数中(不完全是,因为在一个解决方案中我们必须使用 std::forward
)。有没有办法通过避免重复代码来实现这一点?此功能很小,但在其他情况下可能会更大。
总结起来有两个问题:
为什么 func2
函数在收到 rvalue 或 rvalue 引用 时未优化以调用移动赋值?
我如何修改 func3
函数才能避免“复制”代码?
编辑以澄清我在
我明白第一点(为什么编译器不优化这一点)。它只是根据语言的定义如何工作,编译器不能简单地优化它,因为在每种情况下必须调用哪些运算符已经明确定义并且必须遵守。程序员期望某些运算符被调用,而优化尝试会不可预测地改变调用它们的方式和方式。我遇到的唯一例外是 Return 值优化 (RVO),其中编译器可以消除为保存函数的 return 值而创建的临时对象;以及可以应用 Copy Elision 来消除不必要的对象复制的情况。根据其wikipedia article,优化不能应用于已绑定到引用的临时对象(我认为这正是适用于我们的情况):
Another widely implemented optimization, described in the C++ standard, is when a temporary object of class type is copied to an object of the same type. As a result, copy-initialization is usually equivalent to direct-initialization in terms of performance, but not in semantics; copy-initialization still requires an accessible copy constructor. The optimization can not be applied to a temporary object that has been bound to a reference.
关于避免重复代码,我尝试了建议的 SO 帖子 (func3
),因为当将 rvalue 或 rvalue reference 传递给函数时它们可以正常工作,但它们不会传递 lvalue.
为了对此进行测试,考虑到原始代码,我们添加了两个函数 func4
和 func5
来实现建议的解决方案:
template<typename T>
inline constexpr void func4(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func4(T &el, const T &c) {
T copy = c;
func4(el, std::move(copy));
}
template<class T>
std::decay_t<T> copy(T&& t) {
return std::forward<T>(t);
}
template<typename T>
inline constexpr void func5(T &el, T &&c) {
el = std::forward<T>(c);
}
template<typename T>
inline constexpr void func5(T &el, const T &c) {
func5(el, copy(c));
}
与原始函数一样,我们使用 rvalue、lvalue 和 rvalue 引用来调用这些函数(std::move(lvalue)
),结果如下:
==================================================================
------------- func4 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func4 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
------------- func5 with rvalue ----------------------------------
MyClass(int n) [custom constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with lvalue ----------------------------------
MyClass(const MyClass& other) [copy constructor]
MyClass& operator=(MyClass&& other) [move assignment]
~MyClass() [destructor]
------------- func5 with std::move(lvalue) -----------------------
MyClass& operator=(MyClass&& other) [move assignment]
==================================================================
在lvalue的情况下,不是直接调用拷贝赋值,而是调用拷贝构造函数创建一个临时对象,然后调用移动赋值运算符;这比只调用复制赋值运算符而不创建临时对象(这是 func3
通过复制代码所做的)效率更低。
据我了解,目前还没有完全等效的方法来避免代码重复。
想想下面的例子:
void foo(int) {}
void foo(double) {}
void bar(double x) {
foo(x);
}
int main() {
bar(0);
}
在上面的程序中,总是会调用foo(double)
,而不是foo(int)
。这是因为虽然参数最初是 int
,但一旦您进入 bar
,此信息就无关紧要了。 bar
只看到它自己的参数 x
,无论原始参数类型是什么,它的类型都是 double
。因此,它调用与参数类型 x
.
foo
的重载
您的 func2
工作方式类似:
void func2(MyClass &el, const MyClass &c) {
el = c;
}
此处,表达式 c
是一个左值,即使引用可能在调用时已绑定到临时对象。因此,编译器必须 select =
将左值作为其右参数的运算符。
为了将左值转发为左值,将右值转发为右值,经常使用 const MyClass&
和 MyClass&&
重载,即使(如您所见)它是重复的。有关如何减少代码重复的一些建议,请参阅