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;
}
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.
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 add D::~D)a is never used after constructing d and it would be better if it could relinquish its buffer and be "consumed".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.char* with std::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 how std::unique_ptr could solve this, or std::string, or again std::vector.D requires 0/3/5 and how can we also fix that.return {a,b,c} and the subsequent construction of dd.D dd not calling the assignment operator.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.
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));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With