2013-03-24 61 views
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 = (...) 

回答

1

你的运营商实现由价值/左值参考返回。 这会导致链接操作接受对象副本(因此是copy ctor)或左值引用。

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

2

首先A() + c返回左值参考。这使得表达式本身成为左值。

函数调用是一个左值如果结果类型是左值引用类型或一个rvalue参照功能类型,如果结果类型是右值引用到对象类型的x值,和否则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; } 
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 

我不认为你可以期待任何好于只有两个临时工的谁le表达。

关于你注释掉的代码:try std::move(d)而不是纯d,你会安全的d在上面的输出副本,并减少临时工的数量为一。如果您还添加了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; } 
}; 

同时仍然有效,避免任何unneccesary临时工。请享用!

+1

@Chameleon:我创建了一个库来帮助你(和其他人),看到更新后的答案。 – 2013-03-24 15:32:09

+0

图书馆4h:很快! – 2013-03-24 17:34:12

+0

图书馆设计的缺点是,它只适用于你愿意侵入的'struct'和'class'类型。如果你通过SFINAE模板和traits类来完成,你可以用'enum'和'枚举类型(对于位域),对于其他类型非侵入式(重写'A + = B'和'B + = A',然后说它是可交换的,而bob是你的叔叔)。缺点是你最终注入了一堆模板操作符到全局命名空间(因为ADL不能再使用),并且为一些无关的类型生成一些诊断消息很难...... – Yakk 2013-03-24 20:53:19

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)); } 

其中凸点误差达1个步骤中使用SFINAE解析堆栈。 (注意,在该T&&&&A&&具有完全不同的含义 - T&&&&在型扣上下文正在被使用,所以可以T绑定到任何引用类型,而A&&&&未在类型扣除使用上下文,所以它意味着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; 
} 

我这个玩上live work space这里是输出:

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*实际上会创建一个新对象。通过使用AUTOCOPY突出显示了昂贵的操作(创建新对象和复制) - 如您所见,存在最初的4个字母表对象,表达式中的tmp对象以及operator*(double)创建的两个副本。

我们可以摆脱一些副本与此:

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

然而,我们还是最终用不平凡的状态3个物体破坏,因为两次我们做operator+(A&&, A&&),而我却没有假定这个操作是高效的。

如果是,我们可以添加该运营商:

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

,并输出结果显示,只有一个不平凡的状态对象是不断破坏。

实时工作区的最终版本是here。对于递归跟踪,在基本级别,它在函数的末尾打印一个换行符,在更深的递归操作中,它执行-->打印,理论上,如果递归深度足够深,它将打印[括号来帮助)。

最终输出:

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)(*#))} 

在这里你可以看到在最后摧毁了一个“复杂的对象”(其整个历史一起)。

+0

你的*“可爱的技巧”*完全错误***和***危险***!如果输入的'a'不是临时的,'std :: move' *无条件地将它转换为一个可移动的对象,因此最终会毁掉'a'。 – 2013-03-24 13:12:28

+0

@DanielFrey该函数的签名包括'A && a',所以'a'可以移动*已经* - std :: move只是声明'a + = b'的返回值将会是像'a'一样可移动。 'A &&'中的&&不在类型推导上下文中使用,所以它只绑定到可移动的值。如果'A&A :: operator + =(...)'返回的是'* this'以外的内容,那么将会出错 - 这里不是这种情况。右值参考这可以使这种技术更强大。 – Yakk 2013-03-24 13:32:13

+0

哦,对。对不起,我对其他重载是成员的事实感到困惑,在一般情况下我觉得这很不寻常。 – 2013-03-24 13:37:22