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値参照のおかげで、operator +のこれらの4つのオーバーロードにより、一時的なものを再利用することで、作成されるオブジェクトの数を最小限に抑えることができます。しかし、これがもたらすコードの重複は好きではありません。より少ない繰り返しで同じことを達成できますか?

4

4 に答える 4

32

一時的なものをリサイクルすることは興味深いアイデアであり、この理由で右辺値参照を返す関数を作成したのはあなただけではありません。古いC++0xドラフト演算子+(string &&、string const&)でも、右辺値参照を返すように宣言されていました。しかし、これは正当な理由で変更されました。この種のオーバーロードとリターンタイプの選択には3つの問題があります。それらのうちの2つは実際の型に依存せず、3番目の引数は型の種類を参照し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+が、最初の連結中に作成された一時への右辺値参照を返した場合、文字列temporaryが十分に長く存在しないため、このコードは未定義の動作を呼び出します。

  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は、パフォーマンスにおいてまったく何も得られません。

私はすべての追加のオーバーロードを取り除き、古典的な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値参照のおかげで、operator +のこれらの4つのオーバーロードにより、一時的なものを再利用することで、作成されるオブジェクトの数を最小限に抑えることができます。

いくつかの例外を除いて、右辺値参照を返すことは非常に悪い考えです。そのような関数の呼び出しは、prvaluesではなくxvaluesであり、一時的なオブジェクトの存続期間に厄介な問題が発生する可能性があるためです。しないでください。

于 2011-05-15T11:37:59.173 に答える
7

これは、現在のC ++ですでにうまく機能しており、C ++ 0xで移動セマンティクス(使用可能な場合)を使用します。すでにすべてのケースを処理しますが、コピーを回避するためにコピーの省略とインライン化に依存しているため、特に2番目のパラメーターでは、必要以上のコピーが作成される可能性があります。これについての良い点は、他のオーバーロードなしで機能し、(意味的に)正しいことを行うことです:

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)に2つの左辺値を追加したときにコピーを作成することです。

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++を使用してFredNurkの答えをコーディングしました。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++で得た結果です。あなたがそうするかもしれない方法を彼らに解釈してください。そして、あなたのマイレージは変わるかもしれません。

于 2011-05-15T15:31:24.667 に答える