C++11构造函数

4
新的移动构造函数/移动操作符允许我们传递对象的所有权,从而避免使用(昂贵的)复制构造函数调用。但是有没有可能避免构造临时对象(不使用返回参数)?
例如:在下面的代码中,构造函数被调用了4次——但理想情况下,我想做的是避免在cross方法中构造任何对象。使用返回参数(例如void cross(const Vec3 &b, Vec3& out))是可能的,但很难阅读。我有兴趣更新现有变量。
#include <iostream>

using namespace std;

class Vec3{
public:
    Vec3(){
        static int count = 0;
        id = count++;
        p = new float[3];
        cout << "Constructor call "<<id <<" "<<p<< " "<<this<< endl;
    }

    ~Vec3(){
        cout << "Deconstructor call "<<id << " "<<p<<" "<<this<< endl;
        delete[] p;
    }

    Vec3(Vec3&& other)
    : p(nullptr) {
        cout << "Move constructor call "<<id << " "<<p<<" "<<this<< endl;
        p = other.p;
        other.p = nullptr;
    }

    Vec3& operator=(Vec3&& other) {
        cout << "Move assignment operator call from "<<other.id<<" to "<<id << " "<<p<<" "<<this<< endl;
        if (this != &other) {
            p = other.p;
            other.p = nullptr;
        }
        return *this;
    }

    Vec3 cross(const Vec3 &b){
        float ax = p[0], ay = p[1], az = p[2],
            bx = b.p[0], by = b.p[1], bz = b.p[2];
        Vec3 res;
        res.p[0] = ay * bz - az * by;
        res.p[1] = az * bx - ax * bz;
        res.p[2] = ax * by - ay * bx;
        return res;
    }

    float *p;
    int id;
};


int main(int argc, const char * argv[])
{
    Vec3 a,b,c;
    a = b.cross(c);
    return 0;
}

1
在构造函数中移动后,不要忘记将other.p设置为nullptr。使用容器或智能指针来避免这种情况。 - awesoon
1
回复:“转移对象的所有权” - 移动与所有权毫无关系。它涉及到对象的内容,以及在源对象之后不再需要时高效地传输内容。 - Pete Becker
4个回答

4
另一个解决方案是从 a.cross(b) 返回一个“表达式对象”,将计算推迟到该对象被赋值给 c,然后在 operator= 中实际执行计算操作:
 struct Vec3
 {

      CrossProduct cross(const Vec3& b);

      Vec3& operator=(CrossProduct cp)
      {
          do calculation here putting result in `*this`
      }
 }

并添加类似的机制来进行构建等操作。

这更加复杂,但许多C++数学库使用此设计模式。


也许不是最简单的解决方案,但我非常喜欢这个想法 :) - Mortennobel
+1;如果我没记错的话:还有一种方法可以将一系列操作分离并集中在单个函数/操作中...某种连接方式,但我目前找不到。如果我没记错,它还涉及到这样的“表达式对象”。 - Pixelchemist
1
@Pixelchemist:是的,那是正确的。例如,在矩阵运算中,有优化的操作,如 V1 = V2*M + V3。为了识别模式,您可以使用表达式对象来形成解析树,然后一次性在树的适当分支上执行优化操作。 - Andrew Tomazos
你能对这个话题再详细解释一下吗? - Pixelchemist

2

如果您直接分配新值:

Vec3 a = b.cross(c);

有可能会启用RVO,这样就不需要临时构造并在后期移动了。请确保使用优化编译。返回值将被原地构造到 a 中。

此外,在堆上分配一个包含 3 个浮点数的数组似乎会影响性能。使用类 C 数组 float p[3]std::array<float, 3> 应该会更好。


我之前不是很清楚 - 但在我的情况下,我希望叉积更新一个现有的变量。 - Mortennobel
RVO不是编译器优化吗?我希望有一种C++语言特性,可以说“不要创建临时对象,只需更新将分配结果的对象”。 - Mortennobel
@Mortennobel:这是一种标准优化。标准明确允许并指定何时允许进行RVO(据我记得第12.8条款)。我所知的所有生产编译器都在标准规定的情况下实现了RVO,因此您可以依赖它。 - Andrew Tomazos
@Mortennobel:是的,RVO 是一个编译器优化。更新现有对象的唯一方法是使用输出参数(例如按引用传递),正如你在问题中发现的那样。 - Juraj Blaho

1

要更新现有变量,您可以使用输出参数:

// out parameter version
void cross(const Vec3 &b, Vec3& res){
    float ax = p[0], ay = p[1], az = p[2],
        bx = b.p[0], by = b.p[1], bz = b.p[2];
    res.p[0] = ay * bz - az * by;
    res.p[1] = az * bx - ax * bz;
    res.p[2] = ax * by - ay * bx;
    return res;
}

当使用返回值版本作为初始化程序时,RVO将省略构造函数(但在分配给现有对象时不会省略):
// return value version (RVO)
Vec3 cross(const Vec3& b)
{
    Vec3 t; cross(b, t); return t;
}

此外,您可以提供一个结果对象的修改器:
// assignment version
void set_cross(const Vec3& a, const Vec3& b)
{
    a.cross(b,*this);
}

所有三个成员函数可以有效地共存并重用彼此的代码,如图所示。

但是不使用输出参数也可以(高效地)完成吗? - Mortennobel
@Mortennobel:当然!RVO优化在所有现代编译器中都有。 - Gonmator
不是这样的。函数需要知道结果对象在哪里,以便将其数据放在那里。 - Andrew Tomazos
@Gonmator:RVO仅在返回值初始化新对象时起作用,而不是在将结果分配给现有对象时。 - Andrew Tomazos
@user1131467:我的意思是在cross()成员函数中,而不是set_cross()。创建对象t,修改它,然后返回它有什么问题? - Gonmator
@Gonmator:创建一个新的临时对象,然后将其分配给现有对象。这比直接使用目标对象不创建临时对象效率低。在这种情况下,RVO无法帮助,只有out参数可以帮助(无论是作为命名的out参数(cross(b, res))还是作为隐式对象参数(set_cross))。 - Andrew Tomazos

0

这并不是对你问题的直接回答,我只能做出一点贡献,因为现有的答案已经涵盖了重要的观点,但我想引起你对所选择的基于堆的设计的缺点的注意。

3D向量很少单独存在。

当然,这是一种权衡,取决于您需要进行多少次移动以及在向量上执行多少个操作。

如果您只使用一些单个向量并且进行大量的复制/移动操作,那么可以坚持使用基于堆的设计。 但是,如果您有多个向量(例如在向量或数组中)并且希望对它们进行操作,则建议您不要进行堆分配,如果您担心性能问题。

std::vector<Vec3> a(20);
class con_Vec3 { double x, y, z; };
std::vector<con_Vec3> b(20);

a 向量将维护一个指向浮点数的连续指针块,这些浮点值将驻留在内存的其他位置,这意味着您的值实际上是分散在内存中的。 相比之下,向量b 将包含一个连续的 60 个 double 块,全部位于同一地方。

移动这样一个 std::vector 的成本对于两种情况是相等的(几乎相当于交换),但如果你复制它们,基于堆的解决方案将会更慢,因为将进行 21 次分配和 20 次副本,而在非堆解决方案中,有一个分配和 20 次覆盖整个向量的拷贝。


此外,CPU缓存命中可能会增加对con_Vec3循环计算的处理(与每次访问不同元素的堆分配数据的几乎确定的缓存未命中相反)。 - rectummelancolique

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接