15

考虑以下:

struct vec
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z) : v{x,y,z} {};
    vec(const vec& that) = default;
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}
vec&& operator+(vec&& lhs, const vec& rhs)
{
    return move(lhs += rhs);
}
vec&& operator+(const vec& lhs, vec&& rhs)
{
    return move(rhs += lhs);
}
vec&& operator+(vec&& lhs, vec&& rhs)
{
    return move(lhs += rhs);
}

感谢 r-value 引用,通过这四个 operator+ 重载,我可以通过重用临时对象来最小化创建的对象数量。但我不喜欢这里引入的重复代码。我可以用更少的重复来达到同样的效果吗?

4

4 回答 4

32

回收临时对象是一个有趣的想法,并且您不是唯一一个为此编写返回右值引用的函数的人。在较早的 C++0x 草案中 operator+(string&&,string const&) 也被声明为返回一个右值引用。但这有充分的理由发生了变化。我看到这种重载和返回类型选择的三个问题。其中两个与实际类型无关,第三个参数指的是类型的类型vec

  1. 安全问题。考虑这样的代码:

    vec a = ....;
    vec b = ....;
    vec c = ....;
    auto&& x = a+b+c;
    

    如果您的最后一个运算符返回一个右值引用,x则将是一个悬空引用。否则,它不会。这不是一个人为的例子。例如,该auto&&技巧在内部用于 for-range 循环以避免不必要的复制。但是由于引用绑定期间临时对象的生命周期扩展规则不适用于仅返回引用的函数调用,因此您将获得一个悬空引用。

    string source1();
    string source2();
    string source3();
    
    ....
    
    int main() {
      for ( char x : source1()+source2()+source3() ) {}
    }
    

    如果最后一个 operator+ 返回对在第一次连接期间创建的临时值的右值引用,则此代码将调用未定义的行为,因为字符串临时值不会存在足够长的时间。

  2. 在通用代码中,返回右值引用的函数会强制您编写

    typename std::decay<decltype(a+b+c)>::type
    

    代替

    decltype(a+b+c)
    

    仅仅是因为最后一个 op+ 可能返回一个右值引用。以我的拙见,这变得越来越丑陋。

  3. 由于您的类型vec既“扁平”又小,因此这些 op+ 重载几乎没有用处。请参阅 FredOverflow 的答案。

结论:应避免使用具有右值引用返回类型的函数,尤其是当这些引用可能引用短期临时对象时。std::move并且std::forward是此经验法则的特殊用途例外。

于 2011-05-17T12:04:43.717 回答
8

由于您的vec类型是“平面”(没有外部数据),因此移动和复制完全相同。因此,您的所有右值引用和std::moves 对性能毫无帮助。

我会摆脱所有额外的重载,只写经典的引用到常量版本:

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}

如果您对移动语义还知之甚少,我建议您研究这个问题

感谢 r-value 引用,通过这四个 operator+ 重载,我可以通过重用临时对象来最小化创建的对象数量。

除了少数例外,返回右值引用是一个非常糟糕的主意,因为此类函数的调用是 xvalues 而不是 prvalues,并且您可能会遇到令人讨厌的临时对象生命周期问题。不要这样做。

于 2011-05-15T11:37:59.173 回答
7

这在当前的 C++ 中已经很好地工作了,它将在 C++0x 中使用移动语义(如果可用)。它已经处理了所有情况,但依靠复制省略和内联来避免复制——因此它可能会制作比预期更多的副本,特别是对于第二个参数。关于这一点的好处是它可以在没有任何其他重载的情况下工作并且做正确的事情(语义上):

vec operator+(vec a, vec const &b) {
  a += b;
  return a;  // "a" is local, so this is implicitly "return std::move(a)",
             // if move semantics are available for the type.
}

这就是你会停下来的地方,99% 的时间。(我可能低估了这个数字。)这个答案的其余部分只适用于你知道,例如通过使用分析器,来自 op+ 的额外副本值得进一步优化。


为了完全避免所有可能的复制/移动,您确实需要这些重载:

// lvalue + lvalue
vec operator+(vec const &a, vec const &b) {
  vec x (a);
  x += b;
  return x;
}

// rvalue + lvalue
vec&& operator+(vec &&a, vec const &b) {
  a += b;
  return std::move(a);
}

// lvalue + rvalue
vec&& operator+(vec const &a, vec &&b) {
  b += a;
  return std::move(b);
}

// rvalue + rvalue, needed to disambiguate above two
vec&& operator+(vec &&a, vec &&b) {
  a += b;
  return std::move(a);
}

你在正确的轨道上,没有真正的减少可能(AFAICT),但如果你经常需要这个 op+ 用于许多类型,宏或 CRTP 可以为你生成它。唯一真正的区别(我对上述单独语句的偏好很小)是当您在 operator+(const vec& lhs, vec&& rhs) 中添加两个左值时,您会复制:

return std::move(rhs + lhs);

通过 CRTP 减少重复

template<class T>
struct Addable {
  friend T operator+(T const &a, T const &b) {
    T x (a);
    x += b;
    return x;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }
};

struct vec : Addable<vec> {
  //...
  vec& operator+=(vec const &x);
};

现在不再需要专门为 vec 定义任何 op+。Addable 可用于任何带有 op+= 的类型。

于 2011-05-15T05:03:11.400 回答
1

我使用clang + libc++编写了 Fred Nurk 的答案。我不得不删除初始化器语法的使用,因为 clang 还没有实现它。我还在复制构造函数中放置了一条打印语句,以便我们可以计算副本。

#include <iostream>

template<class T>
struct AddPlus {
  friend T operator+(T a, T const &b) {
    a += b;
    return a;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }

};

struct vec
    : public AddPlus<vec>
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z)
    {
        v[0] = x;
        v[1] = y;
        v[2] = z;
    };
    vec(const vec& that)
    {
        std::cout << "Copying\n";
        v[0] = that.v[0];
        v[1] = that.v[1];
        v[2] = that.v[2];
    }
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

int main()
{
    vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3);
    vec v5 = v1 + v2 + v3 + v4;
}

test.cpp:66:22: error: use of overloaded operator '+' is ambiguous (with operand types 'vec' and 'vec')
    vec v5 = v1 + v2 + v3 + v4;
             ~~~~~~~ ^ ~~
test.cpp:5:12: note: candidate function
  friend T operator+(T a, T const &b) {
           ^
test.cpp:10:14: note: candidate function
  friend T&& operator+(T &&a, T const &b) {
             ^
1 error generated.

我像这样修复了这个错误:

template<class T>
struct AddPlus {
  friend T operator+(const T& a, T const &b) {
    T x(a);
    x += b;
    return x;
  }

  friend T&& operator+(T &&a, T const &b) {
    a += b;
    return std::move(a);
  }

  friend T&& operator+(T const &a, T &&b) {
    b += a;
    return std::move(b);
  }

  friend T&& operator+(T &&a, T &&b) {
    a += b;
    return std::move(a);
  }

};

运行示例输出:

Copying
Copying

接下来我尝试了 C++03 方法:

#include <iostream>

struct vec
{
    int v[3];

    vec() : v() {};
    vec(int x, int y, int z)
    {
        v[0] = x;
        v[1] = y;
        v[2] = z;
    };
    vec(const vec& that)
    {
        std::cout << "Copying\n";
        v[0] = that.v[0];
        v[1] = that.v[1];
        v[2] = that.v[2];
    }
    vec& operator=(const vec& that) = default;
    ~vec() = default;

    vec& operator+=(const vec& that)
    {
        v[0] += that.v[0];
        v[1] += that.v[1];
        v[2] += that.v[2];
        return *this;
    }
};

vec operator+(const vec& lhs, const vec& rhs)
{
    return vec(lhs.v[0] + rhs.v[0], lhs.v[1] + rhs.v[1], lhs.v[2] + rhs.v[2]);
}

int main()
{
    vec v1(1, 2, 3), v2(1, 2, 3), v3(1, 2, 3), v4(1, 2, 3);
    vec v5 = v1 + v2 + v3 + v4;
}

运行这个程序根本没有产生任何输出。

这些是我使用clang++得到的结果。用你可能的方式解释它们。你的里程可能会有所不同。

于 2011-05-15T15:31:24.667 回答