我正在尝试在大型对象之间进行操作,并尝试使用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 = (...)
答案 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
你可以看到最后被破坏的单个“复杂对象”(连同它的整个历史)。