Reputation: 88
To better understand copy elision I wrote a test app that did a simple action in copy and move constructors/assignment operators and counted the times it was copied or moved. I noticed however that there was a copy when I created an rvalue and passed it directly rather than creating a lvalue then passing it in.
I'm trying to understand if this is compiler defined or part of the language specification? Additionally I'm trying to understand why it wouldn't be omitted as it seems like it should just be a construction rather than a copy?
struct Bar {
Bar() {}
Bar(const Bar& a)
: cp_count{a.cp_count + 1}, mv_count{a.mv_count} {}
Bar& operator=(const Bar& a) {
cp_count = a.cp_count + 1;
mv_count = a.mv_count;
return *this;
}
Bar(Bar&& a)
: cp_count{a.cp_count}, mv_count{a.mv_count + 1} {}
Bar& operator=(Bar&& a) {
cp_count = a.cp_count;
mv_count = a.mv_count + 1;
return *this;
}
int cp_count = 0;
int mv_count = 0;
};
struct Foo {
Bar bar;
std::function<void(Bar)> setter;
std::function<void(Bar)> setter2;
Foo() {
setter = [this](Bar a) {
bar = a;
};
setter2 = [this](Bar a) {
bar = std::move(a);
};
}
};
int main() {
std::cout << std::endl << "base line" << std::endl;
{
Foo foo;
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "in-place then copy" << std::endl;
{
Foo foo;
foo.setter(Bar{});
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "copy then copy" << std::endl;
{
Foo foo;
Bar bar{};
foo.setter(bar);
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "move then copy" << std::endl;
{
Foo foo;
Bar bar{};
foo.setter(std::move(bar));
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "in-place then move" << std::endl;
{
Foo foo;
foo.setter2(Bar{});
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "copy then move" << std::endl;
{
Foo foo;
Bar bar{};
foo.setter2(bar);
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
std::cout << std::endl << "move then move" << std::endl;
{
Foo foo;
Bar bar{};
foo.setter2(std::move(bar));
std::cout << "mv_count = " << foo.bar.mv_count << " cp_count = " << foo.bar.cp_count << std::endl;
}
}
The output I received from my testing
base line
mv_count = 0 cp_count = 0
in-place then copy
mv_count = 1 cp_count = 1
copy then copy
mv_count = 1 cp_count = 2
move then copy
mv_count = 2 cp_count = 1
in-place then move
mv_count = 2 cp_count = 0
copy then move
mv_count = 2 cp_count = 1
move then move
mv_count = 3 cp_count = 0
Upvotes: 1
Views: 96
Reputation: 9317
The additional moves seen in the output are caused by std::function<void(Bar)>
. Change the definition of Foo
to
struct Foo
{
Bar bar;
void setter(Bar a) { bar = a; }
void setter2(Bar a) { bar = std::move(a); }
};
and the output becomes
base line
mv_count = 0 cp_count = 0
in-place then copy
mv_count = 0 cp_count = 1
copy then copy
mv_count = 0 cp_count = 2
move then copy
mv_count = 1 cp_count = 1
in-place then move
mv_count = 1 cp_count = 0
copy then move
mv_count = 1 cp_count = 1
move then move
mv_count = 2 cp_count = 0
which is what you should expect. For example, for in-place then copy, the parameter a
of setter
is initialized from the argument prvalue Bar{}
(guaranteed copy elision in C++17), then is copy-assigned to bar
(one invocation of Bar
's copy-assignment operator); overall, no moves, one copy.
When setter
is std::function<void(Bar)>
, setter(x)
calls std::function
's operator()(Bar arg)
, which wraps your lambda closure object's operator()(Bar a)
. It basically passes std::forward<Bar>(arg)
to your operator()
, adding one move construction (of a
from std::forward<Bar>(arg)
) to all cases, which explains the results you were seeing.
The additional move construction cannot be elided, since
std::forward<Bar>(arg)
is an xvalue, not a prvalue (no guaranteed copy elision);Bar
's move constructor has side effects;return
, not a throw
, not an exception-declaration).Upvotes: 2