使用 C++ 复制省略和运算符重载

时间:2021-06-14 18:11:32

标签: c++ copy-elision return-value-optimization

我有一个结构,例如:

struct A { 
    double x,y; 
    vector<double> vec; 
};

我想重载加号等运算符,以便我可以执行以下操作:

A a,b,c,d;
//do work to set up the four structs. Then:
d = a + b + c;

性能很重要,因为这些操作将在运行多次的“内部循环”中执行。我担心该行 (a + b + c) 将创建临时对象,因此不必要地将构造函数运行到 'A'。 运行 C++17,编译器一定会使用复制省略来避免创建临时文件吗?我需要运行“d=a+b+c”这一行,而不必运行 A 的构造函数。 >

我知道如果我可以通过写作来避免临时性:

d = a;
d += b;
d += c;

但是,实际上,我将要编写很多带有很长的数学行的代码,并且能够在一行中编写所有内容 (a + b + c) 会方便得多,而不是将其分解为大量的“+=”行。

2 个答案:

答案 0 :(得分:2)

正如评论者所建议的那样,如果您的 operator+ 需要一个临时的,您仍然可以构造一个 vector 并返回它,即使 NRVO 已被拒绝。

但是,如果你想这样做,你可以减少临时创建的数量:

  1. 创建一个右值限定的 operator+ 实现
  2. 使用移动赋值运算符将结果向量移动到 d 中,或使用移动构造函数将临时向量移动到 d 中,而无需

考虑一下:

A operator+(A& a, const A& b){
    A temp = /*...*/;
    return temp; // NRVO
}

A operator+(A&& a, const A& b){
    // We know a is temporary, so we can move its guts to a new one.
    a += b;
    return std::move(a); // In this case, we require the move, as it's not NRVO
}

// Then use it:
A d = a + b + c;
// What this does: 
// 1. Calls `a + b` and creates a temporary, as both are lvalues
// 2. Utilizes the same temporary to put the result of (a+b) + c
// 3. Still utilizes the same temporary to materialize the rvalue into d

答案 1 :(得分:0)

感谢 Joel Filho 建议使用表达式模板,以及对相关 Wikipedia article 的参考。维基百科文章中的这种方法有效,尽管必须针对我的特定情况稍作修改,因为我使用的是命名的类成员,而不是实现向量。下面是一个实现。

template<typename E>
class AExpression {
public:
    AExpression() {};

    double Returna() const {
        return static_cast<E>(*this).Returna();
    };
    double Returnb() const {
        return static_cast<E>(*this).Returnb();
    };
};

struct A : public AExpression<A> {
    A() : a(kNaN), b(kNaN) { };
    double a,b;
    double Returna() const {
        return a;
    };
    double Returnb() const {
        return b;
    };
    
    template <typename E>
    A(AExpression<E> & expr) {
        a = expr.Returna();
        b = expr.Returnb();
    };
    
    //this method is needed because the return value of a line such as z = x + y is ASum, not A. This equality overload allows the code to convert back from type ASum to type A
    template <typename E>
    A & operator=(E const & expr) {
        a = expr.Returna();
        b = expr.Returnb();
        return *this;
    }
};

template <typename E1, typename E2>
class ASum : public AExpression<ASum<E1, E2>>  {
    E1 const& one;
    E2 const& two;

public:
    ASum(E1 const& onein, E2 const& twoin) : one(onein), two(twoin) { };

    double Returna() const {
        return one.Returna() + two.Returna();
    };
    double Returnb() const {
        return one.Returnb() + two.Returnb();
    };

};

template <typename E1, typename E2>
ASum<E1, E2>
operator+(AExpression<E1> const& u, AExpression<E2> const& v) {
   return ASum<E1, E2>(*static_cast<const E1*>(&u), *static_cast<const E2*>(&v));
}

使用该实现,如果我运行以下几行,它会执行“z = x + y”而不创建任何临时文件。

A x,y,z;
x.a = 1;
x.b = 2;
y.a = 3;
y.b = 4;

z = x + y;
//z is type 'A' and z.a = 4; z.b = 6; no temporaries are created in the line 'z = x + y'