R 值引用和代码重复的重载

Overloading on R-value references and code duplication

本文关键字:重载 代码 引用      更新时间:2023-10-16

请考虑以下事项:

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

回收临时变量是一个有趣的想法,因此,您不是唯一编写返回右值引用的函数的人。在较旧的 C++0x 草稿中,运算符 +(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() ) {}
    }
    

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

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

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

    而不是

    decltype(a+b+c)
    

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

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

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

由于您的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]);
}

如果你对移动语义还不太了解,我建议你研究这个问题。

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

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

这在当前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可以为你生成它。 唯一真正的区别(我对上面单独语句的偏好很小)是当你在运算符+(const vec& lhs, vec&& rhs)中添加两个lvalue时,你会复制副本:

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+= 的任何类型都可以重用。

我用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 << "Copyingn";
        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 << "Copyingn";
        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++ 得到的结果。 如何解释它们。 而且您的里程可能会有所不同。