有没有理由用rvalue引用重载运算符?

时间:2017-08-08 20:14:35

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

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

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& left, const Vector& right)优化简单代码,并且没有任何理由重载rvalue?

2 个答案:

答案 0 :(得分:2)

如果你的向量移动比复制更便宜(通常在内部存储指向可以廉价复制的数据的指针的情况下),而不是rvlaue引用的重载会有助于提高性能。您可以查看std::string以及它如何对其中的所有可能引用进行重载:http://en.cppreference.com/w/cpp/string/basic_string/operator%2B

修改

由于OP澄清了内部数据表示是一个c风格的数组,因此提供多个重载没有任何好处。 C风格的数组不能便宜地移动 - 而是复制它们 - 所以那些多次重载没有用处。

答案 1 :(得分:1)

这取决于您Vector

的实施情况
  • 如果班级移动比复制更快,为移动提供额外的重载可能会带来性能提升。
  • 否则,提供重载应该不会更快。

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

template <typename T, size_t D>
class Vector
{
   T data[D];
   // ...
};

从您的代码中,我还假设T是一种简单的算术类型(例如,floatint),其中复制速度与移动速度一样快。在这种情况下,您无法实现Vector<float, D>的移动操作,这将比复制操作更快。

要使移动操作比复制更快,您可以更改Vector类的表示形式。您可以存储指向数据的指针,而不是存储C数组,如果大小D很大,则可以进行更有效的移动操作。

作为类比,您当前的Vector实现类似于std::array<T, D>(内部保存了一个c数组,需要复制),但您可以切换到std::vector<T>(它包含指向堆的指针并且易于移动)。值D越大,从std::array切换到std::vector的吸引力就越大。

让我们仔细研究为移动操作提供重载时的差异。

改进:就地更新

您的重载优势在于您可以使用就地更新,以避免必须为operator+(&,&)实施中的结果创建结果副本:

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+(&,&))更快。

编译器可以自动执行就地更新优化吗?

如果没有帮助,编译器很可能无法完成。

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

<强>摘要

如果Vector移动速度快于复制,则从纯粹的性能角度为rvalues提供重载是合理的。如果移动速度不快,则提供过载没有任何好处。