首先,我为这个过于冗长的问题道歉。我想不出任何其他方法来准确总结我的问题......现在进入实际问题:
我目前正在尝试使用 C++0x 右值引用...以下代码会产生不需要的行为:
#include <iostream>
#include <utility>
struct Vector4
{
float x, y, z, w;
inline Vector4 operator + (const Vector4& other) const
{
Vector4 r;
std::cout << "constructing new temporary to store result"
<< std::endl;
r.x = x + other.x;
r.y = y + other.y;
r.z = z + other.z;
r.w = w + other.w;
return r;
}
Vector4&& operator + (Vector4&& other) const
{
std::cout << "reusing temporary 2nd operand to store result"
<< std::endl;
other.x += x;
other.y += y;
other.z += z;
other.w += w;
return std::move(other);
}
friend inline Vector4&& operator + (Vector4&& v1, const Vector4& v2)
{
std::cout << "reusing temporary 1st operand to store result"
<< std::endl;
v1.x += v2.x;
v1.y += v2.y;
v1.z += v2.z;
v1.w += v2.w;
return std::move(v1);
}
};
int main (void)
{
Vector4 r,
v1 = {1.0f, 1.0f, 1.0f, 1.0f},
v2 = {2.0f, 2.0f, 2.0f, 2.0f},
v3 = {3.0f, 3.0f, 3.0f, 3.0f},
v4 = {4.0f, 4.0f, 4.0f, 4.0f},
v5 = {5.0f, 5.0f, 5.0f, 5.0f};
///////////////////////////
// RELEVANT LINE HERE!!! //
///////////////////////////
r = v1 + v2 + (v3 + v4) + v5;
return 0;
}
结果输出
构造新的临时对象来存储结果
构造新的临时对象来存储结果
重用临时的第一个操作数来存储结果
重用临时的第一个操作数来存储结果
虽然我曾希望像
构造新的临时
操作数来存储结果 重用临时的第一个操作数来存储结果
重用临时的第二个操作数来存储结果
重用临时的第二个操作数来存储结果
在尝试重新制定编译器正在执行的操作之后(我正在使用带有选项 -std=c++0x 的 MinGW G++ 4.5.2 以防万一),它实际上看起来很合乎逻辑。该标准说,相等优先级的算术运算从左到右进行评估/分组(为什么我假设从右到左我不知道,我想这对我来说更直观)。所以这里发生的事情是编译器首先评估子表达式(v3 + v4)
(因为它在括号中?),然后开始从左到右将表达式中的操作与运算符重载进行匹配,导致对子表达式的Vector4 operator + (const Vector4& other)
调用表达v1 + v2
. 如果我想避免不必要的临时性,我必须确保不超过一个左值操作数出现在任何带括号的子表达式的紧靠左侧,这对于使用这个“库”并且无辜地期待的任何人来说都是违反直觉的最佳性能(如最小化临时创建)。
(我知道我的代码中关于何时添加到 的结果中存在歧义operator + (Vector4&& v1, const Vector4& v2)
,从而导致警告。但在我的情况下它是无害的,我不想为两个右值引用操作数添加另一个重载- 有人知道是否有办法在 gcc 中禁用此警告?)operator + (Vector4&& other)
(v3 + v4)
v1 + v2
长话短说,我的问题归结为:是否有任何方式或模式(最好是独立于编译器)这个向量类可以被重写以允许在表达式中任意使用括号,这仍然会导致运算符重载的“最佳”选择(最佳就“性能”而言,即最大化与右值引用的绑定)?也许我要求太多了,这是不可能的……如果是这样,那也没关系。我只是想确保我没有遗漏任何东西。
提前谢谢了
附录
首先感谢我在几分钟内得到的快速回复(!) - 我真的应该早点开始在这里发帖......
在评论中回复变得乏味,所以我认为澄清我对这个类设计的意图是有序的。如果有的话,也许你可以指出我思维过程中的一个基本概念缺陷。
您可能会注意到我在类中没有任何资源,例如堆内存。它的成员甚至只是标量类型。乍一看,这使它成为基于移动语义的优化的可疑候选者(另请参阅这个问题,它实际上帮助我很好地掌握了右值引用背后的概念)。
然而,由于这个类应该是一个原型的类将在性能关键的上下文中使用(准确地说是 3D 引擎),所以我想优化每一个可能的小事情。低复杂度算法和与数学相关的技术(如查找表)当然应该构成大部分优化,因为其他任何东西都只是解决症状而不是消除性能不佳的真正原因。我很清楚这一点。
顺便说一句,我的意图是使用向量和矩阵优化代数表达式,这些向量和矩阵本质上是普通的旧数据结构,其中没有指向数据的指针(主要是由于堆上的数据存在性能缺陷[有取消引用其他指针、缓存注意事项等])。
我不关心移动赋值或构造,我只是不希望在评估复杂代数表达式期间创建比绝对必要的更多临时变量(通常只有一个或两个,例如矩阵和向量)。
这些是我的想法,可能是错误的。如果是,请纠正我:
- 要在不依赖 RVO 的情况下实现这一点,必须通过引用返回(再次提醒:请记住,我没有远程资源,只有标量数据成员)。
- 通过引用返回使函数调用表达式成为左值,暗示返回的对象不是临时对象,这是不好的,但通过右值引用返回使函数调用表达式成为 xvalue(见 3.10.1),这在我的方法的背景(见 4)
- 通过引用返回是危险的,因为对象的生命周期可能很短,但是:
- 临时对象可以保证在创建它们的表达式的计算结束之前一直存在,因此:
- 如果该右值引用参数所引用的对象是通过引用返回的对象,则使从那些将至少一个右值引用作为其参数的运算符通过引用返回是安全的。所以:
- 任何仅使用二元运算符的任意表达式都可以通过在涉及不超过一种类似 PoD 的类型时仅创建一个临时值来评估,并且二元运算本质上不需要临时值(如矩阵乘法)
(通过右值引用返回的另一个原因是,就函数调用表达式的右值而言,它的行为类似于按值返回;并且运算符/函数调用表达式必须是右值才能绑定到随后调用采用右值引用的运算符。如 (2) 中所述,对通过引用返回的函数的调用是左值,因此将绑定到具有签名的运算符T operator+(const T&, const T&)
,从而导致创建不必要的临时)
我可以通过使用 C 风格的函数方法来实现所需的性能add(Vector4 *result, Vector4 *v1, Vector4 *v2)
,但是来吧,我们生活在 21 世纪......
总之,我的目标是创建一个向量类,它使用重载运算符实现与 C 方法相同的性能。如果这本身是不可能的,那我想也无济于事。但如果有人能向我解释为什么我的方法注定要失败,我将不胜感激(当然,从左到右的操作员评估问题是我发表帖子的最初原因)。
事实上,我一直在使用“真正的”矢量类,这是一个简化了一段时间,到目前为止没有任何崩溃或损坏的内存。事实上,我从来没有真正返回本地对象作为引用,所以不应该有任何问题。我敢说我所做的是符合标准的。
当然,对原始问题的任何帮助也将不胜感激!
非常感谢所有的耐心再次