4

大きなオブジェクト間で操作を実行しようとしていますが、一時的なオブジェクトの作成を避けるために r 値参照を試しています。実験は次のコードですが、結果は期待したものではありません。

コード:

#include <iostream>
using namespace std;

struct A
{
    A() = default;
    A(const A& a) { cout << "copy ctor" << endl; }
    A(A&& a) { cout << "move ctor" << endl; }
    A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
    A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
    A &operator*=(double s) { cout << "this = this *= s" << endl; return *this; }
    A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }
    A &operator+=(const A &b) { cout << "this = this + const A&" << endl; return *this; }
    A operator+(const A &b) const { cout << "A = const this + const A&" << endl; return *this; }
    A &operator+(A &&b) const { cout << "A&& = const this + A&& --> "; return b += *this; }
};
A &operator+(A &&a, const A &b) { cout << "A&& = A&& + const A& --> "; return a += b; }
A &operator*(A &&a, double s) { cout << "A&& = A&& * s --> "; return a *= s; }

int main()
{
    A a,b,c,d;
    a = b + a * 4 + /*operator*(static_cast<A&&>(d), 2)*/ d * 2 + (A() + c) * 5;

    return 0;
}

出力:

A&& = A&& + const A& --> this = this + const A&     // A() + c
A = const this * s                  // (...) * 5
copy ctor                       // ???
A = const this * s                  // d * 2
copy ctor                       // ???
A = const this * s                  // a * 4
copy ctor                       // ???
A&& = const this + A&& --> this = this + const A&   // (d*2) + (...)
A&& = const this + A&& --> this = this + const A&   // (a*4) + (...)
A&& = const this + A&& --> this = this + const A&   // b + (...)
copy assign                     // a = (...)

私が期待するもの:

A&& = A&& + const A& --> this = this + const A&     // A() + c
A&& = A&& * s --> this = this *= s          // (...) * 5
A&& = A&& * s --> this = this *= s          // (...) * 2    d is not used anymore, so I want to move semantics
A = const this * s      // a * 4    a is not used anymore, but I want to keep semantics
A&& = A&& + const A& --> this = this + const A& // (d*2) + (...)
A&& = A&& + const A& --> this = this + const A& // (a*4) + (...)
A&& = A&& + const A& --> this = this + const A& // b + (...)
move assign     // a = (...)
4

4 に答える 4

4

より少ないコピーでより正確なバージョンを次に示します。

#include <iostream>
#include <utility>
using namespace std;

struct A
{
  A() = default;
  A(const A& a) { cout << "copy ctor" << endl; }
  A(A&& a) { cout << "move ctor" << endl; }
  A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
  A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
  A &operator*=(double s) { cout << "this *= s" << endl; return *this; }
  A &operator+=(const A &b) { cout << "this += const A&" << endl; return *this; }
};

A&& operator+(A &&a, const A &b)
{ cout << "A&& + const A&" << endl; a+=b; return std::move(a); }

A&& operator+(A &&a, A &&b)
{ cout << "A&& + A&&" << endl; a+=b; return std::move(a); }

// I assume commutativity
A&& operator+(const A &a, A &&b)
{ cout << "const A& + A&&" << endl; b+=a; return std::move(b); }

A operator+(const A &a, const A &b)
{ cout << "const A& + const A&" << endl; A r(a); r+=b; return r; }

A&& operator*(A &&a, double s)
{ cout << "A&& * s" << endl; a*=s; return std::move(a); }

A operator*(const A& a, double s)
{ cout << "const A& * s" << endl; A r(a); r*=s; return r; }

int main()
{
  A a,b,c,d;
  a = b + a * 4 + d * 2 + (A() + c) * 5;

  return 0;
}

tそして、ここにsが作成された一時的なものである(注釈付きの)出力があります:

                       expression level    actual operations
                       ----------------    -----------------
const A& * s           t1 = a * 4
copy ctor                                  create t1 = copy a
this *= s                                  t1 *= 4
const A& + A&&         b + t1
this += const A&                           t1 += b
const A& * s           t2 = d * 2
copy ctor                                  create t2 = copy d
this *= s                                  t2 *= 2
A&& + A&&              t1 + t2
this += const A&                           t1 += t2
A&& + const A&         A() + c (note: A() is already a temporary)
this += const A&                           A() += c
A&& * s                A'() * 5
this *= s                                  A'() *= 5
A&& + A&&              t1 + A''()
this += const A&                           t1 += A''()
move assign            a = t1              a = t1

式全体で 2 つの一時的なものよりも優れたものを期待できるとは思いません。

コメントアウトされたコードについて:std::move(d)プレーンの代わりに試してみると、上記の出力dのコピーが安全になりd、一時的な数が 1 つに減ります。も追加するstd::move(a)と、一時的な!

また、 and がないstd::move(d)std::move(a)、コンパイラはそれらのオブジェクトを移動する必要がある/移動できるという手がかりがないため、最終的にそれらを移動するコードは危険であり、明らかに間違っていることに注意してください。


更新:アイデアをライブラリにまとめました。GitHub で見つけください。これにより、コードは次のように単純になります。

#include <iostream>
using namespace std;

#include <df/operators.hpp>

struct A : df::commutative_addable< A >, df::multipliable< A, double >
{
  A() = default;
  A(const A& a) { cout << "copy ctor" << endl; }
  A(A&& a) { cout << "move ctor" << endl; }
  A &operator=(const A& a) { cout << "copy assign" << endl; return *this; }
  A &operator=(A&& a) { cout << "move assign" << endl; return *this; }
  A &operator*=(double s) { cout << "this *= s" << endl; return *this; }
  A &operator+=(const A &b) { cout << "this += const A&" << endl; return *this; }
};

効率的でありながら、不必要な一時的な作業を回避します。楽しみ!

于 2013-03-24T11:52:02.580 に答える
2

まずA() + c左辺値参照で返します。これにより、式自体が左辺値になります。

関数呼び出しは、結果の型が左辺値参照型または関数型への右辺値参照である場合は左辺値、結果の型がオブジェクト型への右辺値参照である場合は xvalue、それ以外の場合は prvalue です。

左辺値は右辺値参照にバインドできないため、のメンバー バージョンoperator*が選択されます。非メンバー関数は、おそらく値によって返されるはずです。

A operator+(A &&a, const A &b) { cout << "A&& = A&& + const A& --> "; return a += b; }
A operator*(A &&a, double s) { cout << "A&& = A&& * s --> "; return a *= s; }

これにより、結果は引き続き一時オブジェクトを参照する prvalue 式になります。

第 2 に、コピー コンストラクターの呼び出しはoperator、値によって返されるメンバー s によって発生します。これにより、オブジェクトのコピーが作成されます。たとえば、戻るとき、関数からの(...) * 5値をコピーします。*this

A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }
于 2013-03-24T11:24:25.550 に答える
1

演算子は、値/左辺値参照で返すように実装されています。これにより、オブジェクトのコピー (したがってコピー ctor) または左辺値参照のいずれかを受け入れる連鎖操作が発生します。

たとえばb + a * 4、 に等しいb.operator+(a.operator*(4))です。への入力operator+はオブジェクトのコピーになります。

于 2013-03-24T11:23:48.840 に答える
0

メソッドのシグネチャは次のとおりです。

struct A
{
    A() = default;
    A(const A& a);
    A(A&& a);
    A &operator=(const A& a);
    A &operator=(A&& a);
    A &operator*=(double s);
    A operator*(double s) const;
    A &operator+=(const A &b);
    A operator+(const A &b) const;
    A &operator+(A &&b) const;
};
A &operator+(A &&a, const A &b);
A &operator*(A &&a, double s);

ここで問題が発生します。まず、右辺値参照が左辺値に変更されないように、 free は渡された をoperator+返す必要があります。A&&同じことが当てはまりますA &A::operator+(A &&b) const;- を返す必要がありA&&ます。

次に、無料の演算子が演算子に連鎖してい+=ます。ここにかわいいテクニックがあります:

template<typename T>
A&&operator+(A &&a, T&&b){ return std::move(a+=std::forward<T>(b)); }
template<typename T>
A&&operator*(A &&a, T&&b){ return std::move(a*=std::forward<T>(b)); }

ここで、引数を操作に盲目的に転送します+=

autoこれは、戻り値の手法を使用して、エラーに関してより堅牢にすることができます。

template<typename T>
auto operator+(A &&a, T&&b)->declval(std::move(a+=std::forward<T>(b)))
{ return std::move(a+=std::forward<T>(b)); }
template<typename T>
auto operator*(A &&a, T&&b)->declval(std::move(a*=std::forward<T>(b)))
{ return std::move(a*=std::forward<T>(b)); }

これは、SFINAE を使用して解析スタックでエラーを 1 ステップ上に上げます。&&( inT&&A&&はまったく異なる意味を持つことに注意してください--は型推論コンテキストで使用されているため、任意の参照型にバインドできT&&ますが、 は型推論コンテキストでは使用されていないため、にバインドすることを意味します右辺値)。&&TA&&&&A&&

次に続くのは、正確さと効率の両方のためにいくつかの基本的な変更を加えた、はるかに大幅にマークアップされたバージョンです。フィールド内の各インスタンスの履歴を追跡します。nameこのフィールドの操作は「本物」ではなく、その値は特定のインスタンスを作成するために必要な「計算」を表します。

移動操作はこの状態を移動すると仮定します。

#include <iostream>
#include <utility>

struct A;
A &operator+=(A& a, std::string op);
A&&operator+=(A&& a, std::string op);

struct recurse_nl {
   int& count() {
      static int v = 0;
      return v;
   }
   recurse_nl(){if (++count()>1) std::cout << " --> "; else if (count()>2) std::cout << " --> [";}
   ~recurse_nl(){if (--count() == 0) std::cout <<"\n"; else if (count()>1) std::cout << "]"; }
};
struct A
{
    std::string name;
    A() = delete;
    A(std::string n):name(n) { recurse_nl _; std::cout << "AUTO ctor{"<<name<<"}";};
    A(const A& o):name(o.name+"_c&") { recurse_nl _; std::cout << "COPY ctor{"<<name<<"}(const&)"; }
    A(A&& o):name(std::move(o.name)) { recurse_nl _; std::cout << "ctor{"<<name<<"}(&&)"; }
    A(A& o):name(o.name+"_&") { recurse_nl _; std::cout << "COPY ctor{"<<name<<"}(&)"; }
    A &operator=(const A& rhs) { recurse_nl _; std::cout << "COPY assign{"<<name<<"}={"<<rhs.name<<"}"; this->name = rhs.name; return *this; }
    A &operator=(A&& rhs) { recurse_nl _; std::cout << "move assign{"<<name<<"}={"<<rhs.name<<"}"; this->name = std::move(rhs.name); return *this; }
    A &operator*=(double d) { recurse_nl _; std::cout << "this{"<<name<<"} *= s{"<<d<<"}"; return (*this) += "(*#)"; }
    A operator*(double d) const { recurse_nl _; std::cout << "A = const this{"<<name<<"} * s{"<<d<<"}"; A tmp(*this); return std::move(tmp*=d); }
    A &operator+=(const A &rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += const A&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=rhs.name+")"; }
    A operator+(const A &rhs) const { recurse_nl _; std::cout << "A = const this{"<<name<<"} + const A&{"<<rhs.name<<"}"; return std::move(A(*this)+="(+)"); }
    A&& operator+(A &&rhs) const { recurse_nl _; std::cout << "A&& = const this{"<<name<<"} + A&&{"<<rhs.name<<"}"; return std::move(rhs += *this); }
    ~A() { recurse_nl _; std::cout << "dtor{"<<name<<"}"; }
};

A &operator+=(A& a, std::string op)
{ a.name+=op; return a; }
A&&operator+=(A&& a, std::string op)
{ a.name+=op; return std::move(a); }

template<typename T>
struct ref_type_of {
   std::string value() const { return "value"; }
};
template<typename T>
struct ref_type_of<T&> {
   std::string value() const { return "&"; }
};
template<typename T>
struct ref_type_of<T&&> {
   std::string value() const { return "&&"; }
};
template<typename T>
struct ref_type_of<T const&&> {
   std::string value() const { return " const&&"; }
};
template<typename T>
struct ref_type_of<T const&> {
   std::string value() const { return " const&"; }
};
template<typename T>
std::string ref_type() { return ref_type_of<T>().value(); }

template<typename T>
A&& operator+(A &&a, T&& b) { recurse_nl _; std::cout << "A&&{"<<a.name<<"} = A&&{"<<a.name<<"} + T" << ref_type<T>(); return std::move(a += std::forward<T>(b)); }
template<typename T>
A&& operator*(A &&a, T&& b) { recurse_nl _; std::cout << "A&&{"<<a.name<<"} = A&&{"<<a.name<<"} * T" << ref_type<T>(); return std::move(a *= std::forward<T>(b)); }

void test1()
{
    A a("a"),b("b"),c("c"),d("d");
    a = b + a * 4 + d * 2 + (A("tmp") + c) * 5;
}
int main()
{
    std::cout << "test1\n";
    test1();
    return 0;
}

私はライブワークスペースでこれを試しましたが、出力は次のとおりです。

stdout:
test1
AUTO ctor{a}
AUTO ctor{b}
AUTO ctor{c}
AUTO ctor{d}
AUTO ctor{tmp}
A&&{tmp} = A&&{tmp} + T& --> this{tmp} += const A&{c}
A&&{tmp(+=c)} = A&&{tmp(+=c)} * Tvalue --> this{tmp(+=c)} *= s{5}
A = const this{d} * s{2} --> COPY ctor{d_c&}(const&) --> this{d_c&} *= s{2} --> ctor{d_c&(*#)}(&&) --> dtor{}
A = const this{a} * s{4} --> COPY ctor{a_c&}(const&) --> this{a_c&} *= s{4} --> ctor{a_c&(*#)}(&&) --> dtor{}
A&& = const this{b} + A&&{a_c&(*#)} --> this{a_c&(*#)} += const A&{b}
A&&{a_c&(*#)(+=b)} = A&&{a_c&(*#)(+=b)} + Tvalue --> this{a_c&(*#)(+=b)} += const A&{d_c&(*#)}
A&&{a_c&(*#)(+=b)(+=d_c&(*#))} = A&&{a_c&(*#)(+=b)(+=d_c&(*#))} + Tvalue --> this{a_c&(*#)(+=b)(+=d_c&(*#))} += const A&{tmp(+=c)(*#)}
move assign{a}={a_c&(*#)(+=b)(+=d_c&(*#))(+=tmp(+=c)(*#))}
dtor{a}
dtor{d_c&(*#)}
dtor{tmp(+=c)(*#)}
dtor{d}
dtor{c}
dtor{b}
dtor{a_c&(*#)(+=b)(+=d_c&(*#))(+=tmp(+=c)(*#))}

これはかなり冗長ですが、ほぼすべての操作を示しています。

operator+必要に応じてoperator*実際に新しいオブジェクトを作成するようにコードを変更しました。コストのかかる操作 (新しいオブジェクトの作成とコピー) は、 and を使用して強調表示さAUTOCOPYます。ご覧のとおり、最初の 4 つのアルファベット オブジェクト、tmp式内のオブジェクト、および によって作成された 2 つのコピーがありoperator*(double)ます。

これでいくつかのコピーを取り除くことができます:

a = b + std::move(a) * 4 + std::move(d) * 2 + (A("tmp") + c) * 5;

ただし、2 回行うため、破棄する重要な状態のオブジェクトが 3 つになることoperator+(A&&, A&&)になり、この操作が非常に効率的であるとは想定していませんでした。

そうであれば、次の演算子を追加できます。

A &operator+=(A &&rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += A&&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=std::move(rhs.name)+")"; }

結果の出力は、非自明な状態のオブジェクトが 1 つだけ破棄されることを示しています。

ライブ ワークスペースの最終バージョンはこちらです。

recurse_nlオブジェクトは再帰追跡用です。基本レベルでは、関数の最後に改行を出力します。より深い再帰では、出力を行います。-->理論的には、再帰が十分に深くなると、[ブラケットを出力して助けになります)。

最終出力:

test1
AUTO ctor{a}
AUTO ctor{b}
AUTO ctor{c}
AUTO ctor{d}
AUTO ctor{tmp}
A&&{tmp} = A&&{tmp} + T& --> this{tmp} += const A&{c}
A&&{tmp(+=c)} = A&&{tmp(+=c)} * Tvalue --> this{tmp(+=c)} *= s{5}
A&&{d} = A&&{d} * Tvalue --> this{d} *= s{2}
A&&{a} = A&&{a} * Tvalue --> this{a} *= s{4}
A&& = const this{b} + A&&{a(*#)} --> this{a(*#)} += const A&{b}
A&&{a(*#)(+=b)} = A&&{a(*#)(+=b)} + Tvalue --> this{a(*#)(+=b)} += A&&{d(*#)}
A&&{a(*#)(+=b)(+=d(*#))} = A&&{a(*#)(+=b)(+=d(*#))} + Tvalue --> this{a(*#)(+=b)(+=d(*#))} += A&&{tmp(+=c)(*#)}
move assign{a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}={a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}
dtor{}
dtor{}
dtor{c}
dtor{b}
dtor{a(*#)(+=b)(+=d(*#))(+=tmp(+=c)(*#))}

最後に、単一の「複雑なオブジェクト」が (その全履歴と共に) 破棄されているのを確認できます。

于 2013-03-24T12:54:48.487 に答える