【问题标题】:Copy elision and operator overloading with C++使用 C++ 复制省略和运算符重载
【发布时间】:2021-08-30 15:42:17
【问题描述】:

我有一个结构,例如:

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 的构造函数。 p>

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

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

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

【问题讨论】:

  • 如果你的operator+ 需要一个临时的来保存a + b 的结果,那么你仍然会调用构造函数。
  • 查看表达式模板。我怀疑复制省略会使表达式模板过时

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


【解决方案1】:

正如评论者所建议的那样,如果您的 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

【讨论】:

  • 非常有趣。感谢您分享这个想法。不过,听起来这种方法仍然会创建一个临时的?我真的需要避免创建任何临时对象(以保证,在最坏的情况下,我可以(不方便地)使用带有 '+=' 的行恢复)。请注意,我的值“d”之前已创建。
  • 是的,这将创建一个临时的,但这仅在d 的向量已经分配到正确的大小时才相关,从而增加了一系列调用中的分配数量。如果d 需要复制a,就像在您的示例中一样,您的分配数量与移动分配完全相同(临时分配,但移动到d 没有)
  • 对,但问题是 d 已经被分配到正确的大小。我将创建 d 一次,存储它,然后一遍又一遍地重复使用它进行计算,这样我就不必进行大量的重新分配。创建一个临时的会强制一个新的堆分配每次调整向量的大小以匹配其他对象,如果这些函数运行多次,这可能会变得很昂贵。
  • 然后你应该看看表达式模板,就像其他评论者指出的那样。 Wikipedia example 正在添加 3 个向量而不分配临时向量,就像您的示例一样。
  • 是的,我认为你是对的。我今天对此进行了实验,并使其正常工作。 Wikipedia 示例与我所学的内容并不是一一对应的,因为我所做的不仅仅是编写一个 Vec 类。但是通过一些实验,让它发挥作用。谢谢!当我有时间复制我为解决这个确切问题而编写的实际代码时,我会尽快写评论。
【解决方案2】:

感谢Joel Filho提出使用表达式模板的建议,并参考相关Wikipedia article。 Wikipedia 文章中的这种方法有效,尽管它必须针对我的特定情况进行轻微修改,因为我使用的是命名类成员,而不是实现向量。下面是一个实现。

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'

【讨论】:

    猜你喜欢
    • 2011-04-17
    • 2010-11-27
    • 1970-01-01
    • 2016-02-19
    • 2011-10-27
    • 2012-12-22
    • 2016-09-26
    • 2012-04-21
    相关资源
    最近更新 更多