'Demonstrating Move Constructor Usefulness
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.
Solution 1:[1]
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:
bis copied and that is okay.Ais 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
ais never used after constructingdand it would be better if it could relinquish its buffer and be "consumed". - Explain the role of
std::moveto 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::vectorgets us rule of zero, which is nice. ccopies the pointer, not the buffer, which is desirable in this case but dangerous because someone has to deallocate it later and howstd::unique_ptrcould solve this, orstd::string, or againstd::vector.- Now some students might ask about whether
Drequires 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 ddnot calling the assignment operator. - At last you can argue that
construct_Dis 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.
Solution 2:[2]
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));
Solution 3:[3]
OK, so after reading up a bit on cppreference, I learned that when the returned value is also a function parameter, the compiler may use move instead of eliding.
from https://en.cppreference.com/w/cpp/language/copy_elision
In a return statement or a throw-expression, if the compiler cannot
perform copy elision but the conditions for copy elision are met or would
be met, except that the source is a function parameter, the compiler will
attempt to use the move constructor even if the object is designated by an lvalue;
And so I think this makes for a pretty convincing demonstration, even in release with optimization flags enabled.
#include <iostream>
using namespace std;
struct A {
int *buff;
A(int x) {
cout << "A::constructor\n";
buff = new int[1000000];
buff[0] = x;
}
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]++; // configure A
return temp; // then return it
}
int main() {
cout << getA(A(7)).buff[0] << endl; // calls move
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | |
| Solution 3 | Gonen I |
