是否有任何理由使用右值引用重载运算符?
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& left, const Vector& right)
优化了简单的代码,没有任何理由重载右值?
【问题讨论】:
不太清楚你的问题是什么。如果您想知道编译器可以优化的内容是否有任何差异,您可以随时查看其输出,例如。这里godbolt.org&&
在模板函数中与 &&
在非模板函数中不同。在模板函数中,&&
是一个 forwarding 引用(并且引用折叠规则适用);非模板函数中的&&
是一个右值引用,并且没有折叠。您有转发参考资料。
@Richard:仅当参数具有 T&& 形式时,在 foo<T>
的情况下(就像这里的情况一样)它是一个实际的 r 值引用。
@DrewDormann,是的,我提到过
@devalone 右值引用可以引用任何对象,而不仅仅是临时对象。此外,如果它是临时数据并且该人写了auto&& result = x + temporary();
,那么结果是一个悬空引用,因为它指的是现在已经死亡的临时数据。
【参考方案1】:
这取决于你对Vector
的实现:
在评论中,您提到Vector
看起来像这样:
template <typename T, size_t D>
class Vector
T data[D];
// ...
;
根据您的代码,我还假设 T
是一种简单的算术类型(例如,float
、int
),其中复制与移动它一样快。在这种情况下,您无法为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
的移动比复制更快,那么从纯粹的性能角度来看,为右值提供重载是合理的。如果移动速度不快,那么提供过载就没有任何好处。
【讨论】:
数组的移动不能比复制慢,所以它不应该让程序变慢。我想对右值使用重载的原因是为了防止在函数中创建额外的对象,但是正如你提到的,有返回值优化,但是当我尝试在 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。
我想使用右值重载的原因是为了防止创建额外的对象。以上是关于是否有任何理由使用右值引用重载运算符?的主要内容,如果未能解决你的问题,请参考以下文章