移动语义以避免临时对象创建

时间:2013-03-24 11:16:03

标签: c++ c++11 operators move-semantics

我正在尝试在大型对象之间进行操作,并尝试使用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 个答案:

答案 0 :(得分: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临时代码的(带注释的)输出:

                       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

我认为你不能指望它比整个表达的两个临时表更好。

关于您已注释掉的代码:尝试使用std::move(d)代替普通d,您将在上述输出中保护d的副本,并将临时数量减少为1。如果您还添加了std::move(a),则整个表达式的评估没有一个临时

另请注意,如果没有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; }
};

虽然仍然有效并且避免任何不必要的临时工。享受!

答案 1 :(得分: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表达式。

其次,复制构造函数调用是由值operator返回的成员引起的。这将导致该对象的副本。例如,当(...) * 5返回时,它会将*this的值复制出函数:

A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }

答案 2 :(得分:1)

您的运营商已实施为按价值 / 按左手价值参考返回。 这导致链接操作接受对象副本(因此复制ctor)或左值引用。

E.g。 b + a * 4等于b.operator+(a.operator*(4))operator+的输入将是对象的副本。

答案 3 :(得分: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);

问题出现在这里。首先,免费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步。 (请注意,&&T&&中的A&&具有完全不同的含义 - T&&的{​​{1}}正在类型推导上下文中使用,因此{ {1}}可以绑定到任何引用类型,而&&的{​​{1}}未在类型推导上下文中使用,因此它意味着T绑定到右值。)。< / p>

接下来是一个标记更为严重的版本,对正确性和效率进行了一些基本的修改。我跟踪A&&字段中每个实例的历史记录 - 该字段的操作不是“真实的”,其值表示创建给定实例所需的“计算”。

我假设移动操作会移动此状态。

&&

我在live work space上播放了这个,这是输出:

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+突出显示昂贵的操作(创建新对象和复制) - 正如您所看到的,有最初的4个字母对象,operator*对象在表达式中,以及由AUTO创建的两个副本。

我们可以用这个删除一些副本:

COPY

然而,我们最终还是有3个具有非平凡状态的对象要销毁,因为我们做了两次tmp,并且我没有假设这个操作是非常有效的。

如果是,我们可以添加此运算符:

operator*(double)

,结果输出显示只有一个具有非平凡状态的对象被破坏。

实时工作空间的最终版本为here

a = b + std::move(a) * 4 + std::move(d) * 2 + (A("tmp") + c) * 5; 对象用于递归跟踪。在基本级别,它在函数末尾打印换行符。在更深的递归时,它执行operator+(A&&, A&&)打印,理论上如果递归变得足够深,它会打印A &operator+=(A &&rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += A&&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=std::move(rhs.name)+")"; } 括号来帮助。)

最终输出:

recurse_nl

你可以看到最后被破坏的单个“复杂对象”(连同它的整个历史)。