【问题标题】:C++11 - Why compiler does not optimize rvalue reference to const lvalue reference binding?C++11 - 为什么编译器不优化对 const 左值引用绑定的右值引用?
【发布时间】:2021-12-22 14:14:28
【问题描述】:

在下一个测试代码中,我们有一个简单的类 MyClass,它只有一个变量成员 (int myValue) 和一个返回 MyClass 新实例的函数 (MyClass getChild())。此类具有重载的主要运算符以在调用时打印。

我们有三个带有两个参数的函数,它们执行简单的赋值(first_param = second_param)::

  • func1:第二个参数是右值引用(也使用std::forward
void func1(MyClass &el, MyClass &&c) {
    el = std::forward<MyClass>(c);
}
  • func2: 第二个参数是一个常量左值引用
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)(或什么是相同的,一个右值引用)。在调用这些函数之前,我们还对 lvaluervaluervalue 引用 进行直接赋值(不调用任何函数)。

测试代码:

#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]

对于赋值,结果如预期。如果你传递一个右值,它调用移动赋值,如果你传递一个左值,它调用复制赋值,如果你传递一个右值引用 em> (std::move(lvalue)) 它调用移动分配。

func1 的调用也是预期的(请记住,此函数接收右值引用)。如果传递一个rvalue,它调用移动赋值,如果你传递一个lvalue,编译失败(因为一个lvalue不能绑定到右值引用),如果你传递一个右值引用(std::move(lvalue)),它会调用一个移动赋值。

但是对于func2,在这三种情况下,都会调用复制赋值。这个函数接收一个常量左值引用作为第二个参数,这是一个左值,然后它调用复制赋值。我理解这一点,但是,为什么编译器在使用时态对象(rvaluervalue reference)调用移动赋值运算符而不是复制作业?

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

总结起来有两个问题:

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

如何修改func3 函数以免“复制”代码?


编辑以澄清我在Brian's answer 之后的思考。

我理解第一点(为什么编译器不优化这个)。这只是语言定义的工作方式,编译器无法简单地优化这一点,因为在每种情况下必须调用哪些运算符已明确定义并且必须得到尊重。程序员希望调用某些运算符,而优化尝试将不可预知地改变它们将被调用的方式和方式。我遇到的唯一例外是返回值优化(RVO),编译器可以消除为保存函数返回值而创建的临时对象;以及可以应用 Copy Elision 来消除不必要的对象复制的情况。根据其wikipedia article,优化不能应用于已绑定到引用的临时对象(我认为这正是适用于我们的情况):

另一个广泛实施的优化,在 C++ 中描述 标准,是将类类型的临时对象复制到 同一类型的对象。因此,复制初始化通常是 在性能方面相当于直接初始化,但不是 在语义上;复制初始化仍然需要一个可访问的副本 构造函数。 优化不能应用于临时对象 已绑定到引用

在避免重复代码方面,我已经尝试了建议的 SO 帖子中的解决方案([1][2]),这在某些情况下可能很方便,但它们不能完全替代具有重复代码的解决方案代码 (func3),因为当将 rvaluervalue 引用 传递给函数时它们会正常工作,但在传递 左值。

为了测试这一点,考虑到原始代码,我们添加了两个函数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]
==================================================================

左值的情况下,不是直接调用拷贝赋值,而是调用拷贝构造操作符创建一个临时对象,然后调用移动赋值操作符;这比只调用复制赋值运算符而不创建临时对象(这是func3 通过复制代码所做的)效率更低。

据我了解,目前没有完全等效的方法来避免代码重复。

【问题讨论】:

  • "...为什么 func2 函数没有优化来调用移动赋值..." - 因为在func2 内部,两个参数都是lvalues,即使@ 987654362@ 被称为rvalues
  • el = c; 的分辨率仅基于表达式 elc 的类型;没有任何关于用于初始化这些变量的表达式的过去历史。 (还要注意表达式没有引用类型)
  • 虽然分析保持性编译器可能很烦人,但不太严格的编译器往往会导致更少的编译器错误和相应的更多运行时错误。至少对于编译器错误,您知道有问题。如果出现运行时错误,您可能不会在为时已晚之前发现问题。

标签: c++ c++11 optimization rvalue-reference lvalue


【解决方案1】:

想想下面的例子:

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 是一个左值,即使在调用时引用可能已经绑定到一个临时对象。因此,编译器必须选择将左值作为右参数的= 运算符。

为了将左值作为左值转发,将右值作为右值转发,经常使用const MyClass&amp;MyClass&amp;&amp; 的重载,尽管(如您所述)它是重复的。有关如何减少代码重复的一些建议,请参阅Is There A Way To Remove Duplicate Code While Providing lvalue and rvalue Overloads?How do I prevent code repeat between rvalue and lvalue member functions?

【讨论】:

  • 我编辑了原始问题以澄清我对这个答案中建议的想法和方法的思考。
猜你喜欢
  • 2017-04-15
  • 1970-01-01
  • 2016-06-09
  • 2017-04-13
  • 2017-05-09
  • 1970-01-01
  • 2021-12-25
  • 2011-02-14
  • 2018-01-13
相关资源
最近更新 更多