I am trying to demonstrate the usefulness of move constructors in eliminating unnecessary copying. However, when I run in Release, the visual studio optimizer elids the copy. No copy constructor is called when a move constructor is not available, and obviously move-constructor is not called when one is added.
I can remove the optimizations by running in Debug, but this does not make for a very convincing demonstration for the need for move semantics.
struct A {
int *buff;
A() {
cout << "A::constructor\n";
buff = new int[1000000];
}
A(const A& a) {
cout << "A::copy constructor\n";
buff = new int[1000000];
memcpy(buff, a.buff, 1000000*sizeof(int));
}
A(A&& original)
{
cout << "Move constructor" << endl;
buff = original.buff;
original.buff = nullptr;
}
~A() { cout << "A::destructor\n"; delete[] buff; }
};
A getA()
{
A temp;
temp.buff[0] = 2;
return temp; // copy on return value
}
void useA(A a1) {
cout << a1.buff[0] << endl;
}
void main() {
useA(getA()); // i'd like copy-constructor to be called if no move constructor is provided
}
Is there anything I can change in the code to prevent the optimizer from being able to do away with the copy, and show how adding a move constructor can prevent a full copy?
Edit: Preferably the demonstration should NOT require an explicit move call/cast.
CodePudding user response:
You can try to add a function that gets A by value:
void f(A a)
{
// ...
}
Then measure the difference between calling it with std::move and without:
A my_a;
// ...
f(my_a);
f(std::move(my_a));
CodePudding user response:
You are running into Named Return Value Optimization(NRVO) which is not mandatory and requires that copy/move ctors are available.
To demonstrate the rule of zero/three/five, move semantics, RVO, copy elision I would use this example:
struct A{
char* large_buffer = new char[1024];
~A(){ delete[] large_buffer;}
};
struct D{
A a;
int b;
int* c;
};
D construct_D()
{
// Obtained somewhere
A a = {};
int b = 10;
int* c = new int[1024];
// Now we want to construct `D` which should "consume" the values.
D d{a,b,c};
// Perhaps do something with that before returning it.
return d;
}
int main(){
D dd = construct_D();
}
You can now explain that:
b
is copied and that is okay.A
is broken because naive copy ctor is member-wise and double delete will happen. ( If one does not forget to addD::~D
)- It can be fixed with rule of three.
- It can be more efficient with rule of five because
a
is never used after constructingd
and it would be better if it could relinquish its buffer and be "consumed". - Explain the role of
std::move
to make that consumption happen. You can rewrite the code interactively to change some variables into temporaries and dig into r-value semantics. - Writing five functions is tedious and replacing
char*
withstd::vector
gets us rule of zero, which is nice. c
copies the pointer, not the buffer, which is desirable in this case but dangerous because someone has to deallocate it later and howstd::unique_ptr
could solve this, orstd::string
, or againstd::vector
.- Now some students might ask about whether
D
requires 0/3/5 and how can we also fix that. - You can add 3/5 and observe that those calls are not made and explain NVRO and RVO for
return {a,b,c}
and the subsequent construction ofdd
. - Also one can always confuse some people with
D dd
not calling the assignment operator. - At last you can argue that
construct_D
is too long so you will interactively refactor it into smaller functions while using all the learned concepts -std::move
, passing by ref,value, RVO.
Overall you can spend a good hour or two just on this example and with the added bonus of spreading the std::
propaganda about the evilness of new
and raw pointers.