Reputation: 2137
I am trying to do operations between large objects and I experiment with r-value references to avoid temporary object creations. The experiment is the following code, but the result is not what I expected.
The code:
#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;
}
The output:
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 = (...)
What I expect:
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 = (...)
Upvotes: 4
Views: 1319
Reputation: 56863
Here's a more correct version with fewer copies:
#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;
}
and here's the (annotated) output with t
s being temporaries created:
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
I don't think you can expect it any better than just two temporaries for the whole expression.
Concerning your commented-out code: try std::move(d)
instead of plain d
and you will safe the copy of d
in the above output and reduces the number of temporaries to one. If you also add std::move(a)
, the whole expression is evaluated without a single temporary!
Also note that without std::move(d)
and std::move(a)
, the compiler has no clue that it should/could move those objects, so any code which ends up moving them anyways is dangerous and plain wrong.
Update: I turned my ideas into a library, find it at GitHub. With this, your code becomes as simple as:
#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; }
};
while still being efficient and avoiding any unneccesary temporaries. Enjoy!
Upvotes: 4
Reputation: 275575
Here is the signatures of your methods:
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);
Problems show up right here. First, free operator+
should return an A&&
that it is passed in, to avoid changing the rvalue reference into an lvalue. The same is true of A &A::operator+(A &&b) const;
-- it should return an A&&
.
Next, your free operators are chaining into the +=
operators. Here is a cute technique:
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)); }
where we blinding forward our arguments to the +=
operation.
This can be made more robust, error-wise, with the auto
return value technique:
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)); }
which bumps errors up 1 step in the parsing stack using SFINAE. (Note that the &&
in T&&
and A&&
have completely different meanings -- T&&
's &&
is being used in a type deduction context, so T
can bind to any reference type, while A&&
's &&
is not being used in a type deduction context, so it means A&&
binds to an rvalue.).
What follows next is a far more heavily marked up version with some basic modifications for both correctness and efficiencies sake. I keep track of the history of each instance in the name
field -- manipulations of this field are not "real", and its value represents the "calculation" required to create a given instance.
I assume move operations move this state.
#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;
}
I played with this on live work space and here is the output:
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)(*#))}
which is pretty verbose, but demonstrates pretty much every operation.
I modified your code so that operator+
and operator*
actually creates a new object when required. The expensive operations (creating a new object, and copying) are highlighted through use of AUTO
and COPY
-- as you can see, there are the initial 4 alphabet objects, the tmp
object in the expression, and two copies created by operator*(double)
.
We can get rid of some of the copies with this:
a = b + std::move(a) * 4 + std::move(d) * 2 + (A("tmp") + c) * 5;
however, we still end up with 3 objects with non-trivial state to destroy because twice we do operator+(A&&, A&&)
, and I didn't assume that this operation is extra efficient.
If it is, we can add this operator:
A &operator+=(A &&rhs) { recurse_nl _; std::cout << "this{"<<name<<"} += A&&{"<<rhs.name<<"}"; return ((*this)+="(+=")+=std::move(rhs.name)+")"; }
and the resulting output shows that only one object with non-trivial state is ever destroyed.
Final version on live workspace is here.
(The recurse_nl
object is for recursion tracking. At the base level, it prints a newline at the end of the function. At deeper recursions, it does the -->
printing, and in theory if recursion got deep enough it would print [
brackets to help).
Final output:
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)(*#))}
where you can see the single "complex object" destroyed at the end (together with its entire history).
Upvotes: 0
Reputation: 18358
Your operators are implemented to return by value / by lvalue reference. This results in chained operations accepting either object copy (hence the copy ctor) or lvalue reference.
E.g. b + a * 4
is equal to b.operator+(a.operator*(4))
. The input to operator+
will be copy of the object.
Upvotes: 1
Reputation: 110668
First of all A() + c
returns by lvalue reference. That makes the expression itself an lvalue.
A function call is an lvalue if the result type is an lvalue reference type or an rvalue reference to function type, an xvalue if the result type is an rvalue reference to object type, and a prvalue otherwise.
An lvalue can't bind to an rvalue reference, so the member version of operator*
is chosen. Your non-member functions should probably be returning by value:
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; }
This causes the result to continue to be a prvalue expression referring to a temporary object.
Second, the copy constructor calls are caused by the member operator
s returning by value. This will cause a copy of the object. For example, when (...) * 5
returns, it will copy the value of *this
out of the function:
A operator*(double s) const { cout << "A = const this * s" << endl; return *this; }
Upvotes: 2