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): :

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;
}

main()函数中我们分别调用这三个函数三次,一次传递一个rvalue,另一个传递一个lvalue,另一个传递 std::move(lvalue)(或者相同的是,右值引用)。在调用这些函数之前,我们还对 lvaluervalue 进行直接赋值(不调用任何函数)右值引用.

测试代码:

#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,然后它调用复制赋值。我理解这一点,但是,为什么编译器在使用时间对象(rvaluervalue 引用)调用时不优化此函数调用移动赋值运算符而不是复制赋值?

func3 试图创建一个与直接赋值工作方式相同的函数,它结合了 func1 行为并定义了具有 func2 行为的重载传递了一个 lvalue。这可行,但此解决方案需要将函数代码复制到两个函数中(不完全是,因为在一个解决方案中我们必须使用 std::forward)。有没有办法通过避免重复代码来实现这一点?此功能很小,但在其他情况下可能会更大。

总结起来有两个问题:

为什么 func2 函数在收到 rvaluervalue 引用 时未优化以调用移动赋值?

我如何修改 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),因为当将 rvaluervalue reference 传递给函数时它们可以正常工作,但它们不会传递 lvalue.

时完全按预期工作

为了对此进行测试,考虑到原始代码,我们添加了两个函数 func4func5 来实现建议的解决方案:

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));
}

与原始函数一样,我们使用 rvaluelvaluervalue 引用来调用这些函数(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