重载R值引用和代码重复

R. *_*des 15 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);
}
Run Code Online (Sandbox Code Playgroud)

感谢r值引用,运算符+这四个重载可以通过重用临时值来最小化创建的对象数量.但我不喜欢这个引入的代码重复.我可以用更少的重复来实现同样的目标吗?

sel*_*tze 32

回收临时值是一个有趣的想法,你并不是唯一一个编写函数来返回rvalue引用的人.在旧的C++ 0x草案操作符+(字符串&&,字符串const&)也被声明为返回右值引用.但这有很好的理由改变了.我看到了这种重载和返回类型选择的三个问题.其中两个独立于实际类型,第三个参数指的是那种类型vec.

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

    vec a = ....;
    vec b = ....;
    vec c = ....;
    auto&& x = a+b+c;
    
    Run Code Online (Sandbox Code Playgroud)

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

    string source1();
    string source2();
    string source3();
    
    ....
    
    int main() {
      for ( char x : source1()+source2()+source3() ) {}
    }
    
    Run Code Online (Sandbox Code Playgroud)

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

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

    typename std::decay<decltype(a+b+c)>::type
    
    Run Code Online (Sandbox Code Playgroud)

    代替

    decltype(a+b+c)
    
    Run Code Online (Sandbox Code Playgroud)

    只是因为最后一个op +可能会返回一个右值引用.在我的拙见中,这变得越来越难看.

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

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

  • Upvoted.很好地总结了这里的危险和历史.我是将这个错误引入早期草稿的罪魁祸首.您可以在这里找到它首先提出:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2002/n1377.htm#string%20Motivation我们在这里用LWG问题修复了它(其中我投票支持):http://www.open-std.org/jtc1/sc22/wg21/docs/lwg-defects.html#1138. (4认同)

fre*_*low 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]);
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


Fre*_*urk 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.
}
Run Code Online (Sandbox Code Playgroud)

这是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);
}
Run Code Online (Sandbox Code Playgroud)

你和你的一起走在正确的轨道上,没有真正的减少(AFAICT),但是如果你经常需要这种op +用于许多类型,宏或CRTP可以为你生成它.唯一真正的区别(我对上面单独的语句的偏好是次要的)是你在operator +中添加两个左值时的副本(const vec&lhs,vec && rhs):

return std::move(rhs + lhs);
Run Code Online (Sandbox Code Playgroud)

通过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);
};
Run Code Online (Sandbox Code Playgroud)

现在不再需要为vec定义任何op +.对于任何具有op + =的类型,Addable都是可重用的.

  • Downvoted.这个答案有误导性,建议使用危险的代码. (3认同)
  • 出于FredOverflow的原因.这个例子应该像在C++ 03中一样编写,尽管默认的成员是一个很好的澄清.没有资源可以搬到这里.移动语义不会减少临时数量.它将资源从一个临时移动到另一个临时.如果您想减少临时数量,请使用表达式模板:http://en.wikipedia.org/wiki/Expression_templates. (2认同)
  • 改变`vec`以便它拥有资源然后我们可以讨论移动语义.你和OP编写的代码只是不必要的混淆代码,如果"认为"正确,只会给rvalue引用增加大量的混淆. (2认同)
  • @HowardHinnant:他们完全按照要求做的事情:尽量减少创建的对象数量.如果你有一个昂贵的副本和移动的类型,你有一个额外的副本无偿添加.我在想数组<int,N>.您不希望不小心引入额外的副本.在风险方面,"const X&x = a + b + c;" 非常罕见.我不认为我见过它.(这应该[熟悉](http://stackoverflow.com/questions/5770253/returning-a-rvalue-reference-in-c0x/5770888#comment-6942937).) (2认同)