重载R值引用和代码重复

时间:2011-05-15 04:39:46

标签: c++ rvalue-reference c++11

请考虑以下事项:

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值引用,运算符+这四个重载我可以通过重用临时值来最小化创建的对象数量。但我不喜欢这个引入的代码重复。我可以用更少的重复来实现同样的目标吗?

4 个答案:

答案 0 :(得分:32)

回收临时表是一个有趣的想法,你并不是唯一一个编写函数的人,因为这个原因会返回rvalue引用。在旧的C ++ 0x草案操作符+(字符串&&,字符串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() ) {}
    }
    

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

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

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

    而不是

    decltype(a+b+c)
    

    只是因为最后一个op +可能会返回一个右值引用。我的拙见认为这很难看。

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

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

答案 1 :(得分:8)

由于您的vec类型是“平坦的”(没有外部数据),因此移动和复制完全相同。所以你所有的右值参考和std::move都会让你绝对没有任何表现。

我将摆脱所有额外的重载,只需编写经典的引用到const版本:

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]);
}

如果您对移动语义知之甚少,我建议您学习this question

  

感谢r值引用,运算符+这四个重载我可以通过重用临时值来最小化创建的对象数。

除了少数例外,返回rvalue引用是一个非常糟糕的主意,因为这些函数的调用是xvalues而不是prvalues,并且你可以得到令人讨厌的临时对象生存期问题。不要这样做。

答案 2 :(得分: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&amp; lhs,vec&amp;&amp; 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 +。对于op + =。

,任何类型的Addable都是可重用的

答案 3 :(得分:1)

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

#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++得到的结果。解释他们你是怎么做的。你的milage可能会有所不同。