是否有任何理由使用右值引用重载运算符?

Posted

技术标签:

【中文标题】是否有任何理由使用右值引用重载运算符?【英文标题】:Is there any reason to overload operators with rvalue reference? 【发布时间】:2017-08-08 20:14:34 【问题描述】:

有模板化的向量类(它是关于数学的,而不是容器)。我需要重载常见的数学运算。像这样超载有什么意义吗:

template <typename T, size_t D>
Vector<T, D> operator+(const Vector<T, D>& left, const Vector<T, D>& right)

    std::cout << "operator+(&, &)" << std::endl;
    Vector<T, D> result;

    for (size_t i = 0; i < D; ++i)
        result.data[i] = left.data[i] + right.data[i];

    return result;


template <typename T, size_t D>
Vector<T, D>&& operator+(const Vector<T, D>& left, Vector<T, D>&& right)

    std::cout << "operator+(&, &&)" << std::endl;
    for (size_t i = 0; i < D; ++i)
        right.data[i] += left.data[i];

    return std::move(right);


template <typename T, size_t D>
Vector<T, D>&& operator+(Vector<T, D>&& left, const Vector<T, D>& right)

    std::cout << "operator+(&&, &)" << std::endl;
    for (size_t i = 0; i < D; ++i)
        left.data[i] += right.data[i];

    return std::move(left);

这个测试代码很好用:

auto v1 = math::Vector<int, 10>(1);
auto v2 = math::Vector<int, 10>(7);
auto v3 = v1 + v2;

printVector(v3);

auto v4 = v3 + math::Vector<int, 10>(2);

printVector(v4);

auto v5 = math::Vector<int, 10>(5) + v4;

printVector(v5);

//      ambiguous overload
//      auto v6 = math::Vector<int, 10>(100) + math::Vector<int, 10>(99);

并打印出来:

operator+(&, &)
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 
operator+(&, &&)
10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 
operator+(&&, &)
15, 15, 15, 15, 15, 15, 15, 15, 15, 15,

两个右值引用有问题,但我认为没关系。

我为什么要这样做?由于性能原因,理论上它会在不创建新对象的情况下更快地工作,但是会吗?也许编译器用operator +(const Vector&amp; left, const Vector&amp; right)优化了简单的代码,没有任何理由重载右值?

【问题讨论】:

不太清楚你的问题是什么。如果您想知道编译器可以优化的内容是否有任何差异,您可以随时查看其输出,例如。这里godbolt.org &amp;&amp; 在模板函数中与 &amp;&amp; 在非模板函数中不同。在模板函数中,&amp;&amp; 是一个 forwarding 引用(并且引用折叠规则适用);非模板函数中的&amp;&amp; 是一个右值引用,并且没有折叠。您有转发参考资料。 @Richard:仅当参数具有 T&& 形式时,在 foo&lt;T&gt; 的情况下(就像这里的情况一样)它是一个实际的 r 值引用。 @DrewDormann,是的,我提到过 @devalone 右值引用可以引用任何对象,而不仅仅是临时对象。此外,如果它是临时数据并且该人写了auto&amp;&amp; result = x + temporary();,那么结果是一个悬空引用,因为它指的是现在已经死亡的临时数据。 【参考方案1】:

这取决于你对Vector的实现:

如果类的移动速度比复制速度快,则为移动提供额外的重载可能会提高性能。 否则,提供重载应该不会更快。

在评论中,您提到Vector 看起来像这样:

template <typename T, size_t D>
class Vector

   T data[D];
   // ...
;

根据您的代码,我还假设 T 是一种简单的算术类型(例如,floatint),其中复制与移动它一样快。在这种情况下,您无法为Vector&lt;float, D&gt; 实施移动操作,这将比复制操作更快。

要使移动操作比复制更快,您可以更改 Vector 类的表示。您可以存储指向数据的指针,而不是存储 C 数组,这样可以在 D 很大的情况下进行更高效的移动操作。

打个比方,您当前的Vector 实现类似于std::array&lt;T, D&gt;(在内部保存一个c 数组并需要复制),但您可以切换到std::vector&lt;T&gt;(它保存指向堆的指针并且易于移动)。 D 的值越大,从std::array 切换到std::vector 应该越有吸引力。

让我们仔细看看为移动操作提供重载时的区别。

改进:就地更新

重载的优点是您可以使用就地更新来避免像在 operator+(&amp;,&amp;) 实现中那样必须为结果创建副本:

template <typename T, size_t D>
Vector<T, D> operator+(const Vector<T, D>& left, const Vector<T, D>& right)

    std::cout << "operator+(&, &)" << std::endl;
    Vector<T, D> result;

    for (size_t i = 0; i < D; ++i)
        result.data[i] = left.data[i] + right.data[i];

    return result;

在您的重载版本中,您可以就地更新:

template <typename T, size_t D>
Vector<T, D>&& operator+(const Vector<T, D>& left, Vector<T, D>&& right)

    std::cout << "operator+(&, &&)" << std::endl;
    for (size_t i = 0; i < D; ++i)
        right.data[i] += left.data[i];

    return std::move(right);

但是,当您使用Vector 的当前实现时,移动结果将导致复制,而在非重载版本中,编译器可以使用return-value optimization 摆脱它。如果您使用类似std::vector 的表示,则移动速度很快,因此就地更新版本应该比原始版本(operator+(&amp;,&amp;))更快。

编译器能否自动进行就地更新优化?

编译器几乎不可能在没有帮助的情况下做到这一点。

在非重载版本中,编译器看到两个数组是常量引用。它很可能能够执行返回值优化,但要知道它可以重用现有对象之一,需要大量额外的知识,而编译器当时并不具备这些知识。

总结

如果Vector 的移动比复制更快,那么从纯粹的性能角度来看,为右值提供重载是合理的。如果移动速度不快,那么提供过载就没有任何好处。

【讨论】:

数组的移动不能比复制慢,所以它不应该让程序变慢。我想对右值使用重载的原因是为了防止在函数中创建额外的对象,但是正如你提到的,有返回值优化,但是当我尝试在 gcc 中使用 -O4 编译时,优化无法进行。跨度> 【参考方案2】:

如果您的向量移动比复制便宜(通常在内部存储指向可以廉价复制的数据的指针时),那么 rvlaue 引用的重载将有助于提高性能。您可以查看std::string 以及它如何在此处的两个参数中为所有可能的引用提供重载:http://en.cppreference.com/w/cpp/string/basic_string/operator%2B

编辑

由于 OP 阐明内部数据表示是 c 样式的数组,因此提供多个重载没有任何好处。 C 风格的数组不能廉价地移动 - 它们被复制 - 所以这些多重重载没有任何用处。

【讨论】:

我的向量只是像 T data[D]; 这样定义的简单数组,它只会用于简单类型,如 int、double 等,我认为向量的大小不会大于 4。 我想使用右值重载的原因是为了防止创建额外的对象。

以上是关于是否有任何理由使用右值引用重载运算符?的主要内容,如果未能解决你的问题,请参考以下文章

何时重载按引用传递(左值和右值)优于按值传递?

是否有任何理由不扩展 std::set 以添加下标运算符?

运算符重载时返回值是否需要添加引用?

右值引用&&

移动选定的构造函数而不是复制。 [参考标准]

C++重载运算符小结与注意点