【问题标题】:How to enforce RVO for operator return value?如何对运算符返回值强制执行 RVO?
【发布时间】:2019-11-28 10:29:06
【问题描述】:

如何在最后 3 个运算符中强制执行 RVO:

#include <iostream>

class Noisy {
    private:
        int m_value;
    public:
        Noisy(int value = 0): m_value(value) 
        {
            std::cout << "Noisy(int)\n";
        }
        Noisy(const Noisy& other): m_value(other.m_value)  
        { 
            std::cout << "Noisy(const Noisy&)\n";
        }
        Noisy(Noisy&& other): m_value(other.m_value)
        { 
            std::cout << "Noisy(Noisy&&)\n";
        }
        //~Noisy() {
        //    std::cout << "dtor\n";
        //}
        Noisy operator+(const Noisy& rhs) &
        {
            std::cout << "+(const Noisy&)&\n";
            return Noisy(m_value + rhs.m_value);
        }
        Noisy operator+(Noisy&& rhs) &
        {
            std::cout << "+(Noisy&&)&\n";
            rhs.m_value += m_value;
            return rhs; //std::move(rhs);
        }
        Noisy operator+(const Noisy& rhs) &&
        {
            std::cout << "+(const Noisy&) &&\n";
            this->m_value += rhs.m_value;
            return *this; //std::move(*this);
        }

        Noisy operator+(Noisy&& rhs) &&
        {
            std::cout << "+(Noisy&&) &&\n";
            this->m_value += rhs.m_value;
            return *this; //std::move(*this);
        }
};

int main()
{
    Noisy a, b, c, d, e, f, g;
    Noisy z = a + b + c + d + e + f + g;

    return 0;
}

程序运行输出:

Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
+(const Noisy&)&
Noisy(int)
+(const Noisy&) &&
Noisy(const Noisy&)
+(const Noisy&) &&
Noisy(const Noisy&)
+(const Noisy&) &&
Noisy(const Noisy&)
+(const Noisy&) &&
Noisy(const Noisy&)
+(const Noisy&) &&
Noisy(const Noisy&) 

或者在最后三个运算符中显式使用std::move 时:

Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
+(const Noisy&)&
Noisy(int)
+(const Noisy&) && 
Noisy(Noisy&&)
+(const Noisy&) &&
Noisy(Noisy&&)
+(const Noisy&) &&
Noisy(Noisy&&)
+(const Noisy&) &&
Noisy(Noisy&&)
+(const Noisy&) &&
Noisy(Noisy&&) 

我不想在操作符中复制,像这样:

Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
Noisy(int)
+(const Noisy&)&
Noisy(int)
+(const Noisy&) &&
+(const Noisy&) &&
+(const Noisy&) &&
+(const Noisy&) &&
+(const Noisy&) &&

到目前为止,我想到的唯一方法是从运算符返回引用,但这显然会导致引用悬空。

我用新鲜的 g++ 在 c++14 和 c++17 中编译。

更新

我知道在不违反规则的情况下强制编译器执行我想要的操作是不可能的。
但是编译器在本地优化右值的 ptevents 是什么? 我想它可以在第一次添加时创建一个右值,在下一次添加中修改它,然后分配给结果变量。

【问题讨论】:

  • 您的最后 3 个运算符返回一个副本。你不能在这里做 RVO,因为那会修改this
  • @user207421 operator+ 应该返回一个新值。但它不应该修改操作数。所以这是错误的,但出于不同的原因。
  • @juanchopanza 如果存在可以修改和返回的右值引用,为什么它应该返回一个新值?
  • @AndreyGodyaev 这些是二进制operator+ 的预期语义。你传递了两件事,你又得到了另一件事。修改任一操作数只会令人困惑。

标签: c++ rvo nrvo


【解决方案1】:

简答

让您的右值引用调用者 + const-reference 参数运算符返回一个右值引用,即 std::moveed 来自 *this

Noisy&& operator+(const Noisy& rhs) &&
{
    std::cout << "+(const Noisy&)&&\n";
    m_value += rhs.m_value;
    return std::move(*this);
}

由于这是从右值引用中触发的,因此您可以随意滥用并将*this 向下移动到调用者,在那里他们可以随意使用它。

示例

#include <iostream>

class Noisy {
private:
    int m_value;
public:
    Noisy(int value = 0) : m_value(value)
    {
        std::cout << "Noisy()\n";
    }
    Noisy(const Noisy& other) : m_value(other.m_value)
    {
        std::cout << "Noisy(const Noisy&)\n";
    }
    Noisy(Noisy&& other) : m_value(other.m_value)
    {
        std::cout << "Noisy(Noisy&&)\n";
        other.m_value = -1;
    }

    ~Noisy()
    {
        std::cout << "~Noisy() : " << m_value << '\n';
    }

    Noisy operator+(const Noisy& rhs) const &
    {
        std::cout << "+(const Noisy&)&\n";
        return Noisy(m_value + rhs.m_value);
    }

    Noisy&& operator+(const Noisy& rhs) &&
    {
        std::cout << "+(const Noisy&)&&\n";
        m_value += rhs.m_value;
        return std::move(*this);
    }

    Noisy operator+(Noisy&& rhs) const
    {
        std::cout << "+(Noisy&&)\n";
        rhs.m_value += m_value;
        return std::move(rhs);
    }
};

int main()
{
    Noisy a, b, c, d, e, f, g;
    std::cout << "========================\n";

    Noisy z = a + b + c + d + e + f + g;
    std::cout << "========================\n";

    return 0;
}

输出

Noisy()
Noisy()
Noisy()
Noisy()
Noisy()
Noisy()
Noisy()
========================
+(const Noisy&)&
Noisy()
+(const Noisy&)&&
+(const Noisy&)&&
+(const Noisy&)&&
+(const Noisy&)&&
+(const Noisy&)&&
Noisy(Noisy&&)
~Noisy() : -1
========================
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0
~Noisy() : 0

注意在a.operator+(b) 的初始构造之后,生成的对象通过右值引用沿链传播。最后一个操作是移动构造到z。 (因此析构函数报告中的-1)。


我已经准备了很长的描述,说明您的操作员链接如何使我要发布的这项工作,但最后变得相当混乱。可以这么说:

Noisy z = a.operator+(b).operator+(c).operator+(....

是您正在做的事情,您需要获取链中所有超过 (b) 的 lhs 操作数,以将其右值引用推送到下一个调用。我展示的操作员将允许这样做。

祝你好运,我希望我能理解你的目标。

【讨论】:

  • Noisy&amp;&amp; x = Noisy(1) + Noisy(1); 对您的操作员版本安全吗?我认为这是一个悬而未决的参考。
  • @AndreyGodyaev 我刚刚看到你的问题中的表达。我以为你在问我发布的示例。由于两个不同的 operator + 实现(提供的一个和 Noisy operator+(Noisy&amp;&amp; rhs) )存在歧义,您提供的代码行甚至无法使用我的示例中的运算符进行编译。您是否在问右值引用生命周期延长是否适用于该运算符?我不认为它会,所以如果这很关键,这将行不通。
  • 因此,如果我想要安全的通用代码,我会被返回Noisy 的运算符所困扰。我还有一个问题:是什么阻止了编译器在优化过程中删除冗余复制?
  • @AndreyGodyaev:事实上,您在复制构造函数中放置了一堆打印语句。副本不是“多余的”;它们是标准要求的。只有当编译器确定代码的可见行为不会改变时,编译器才能删除它们。如果你= default这些构造函数和运算符,那么编译器可以在它认为合适的地方消除它们。
  • @NicolBolas 我看到了有和没有 std::cout 的程序集中会发生什么。编译器很聪明。
猜你喜欢
  • 1970-01-01
  • 2012-11-13
  • 1970-01-01
  • 1970-01-01
  • 2013-01-03
  • 2011-01-03
  • 2017-01-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多