Home > Net >  Demonstrating Move Constructor Usefulness
Demonstrating Move Constructor Usefulness

Time:04-04

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:

  1. b is copied and that is okay.
  2. A is broken because naive copy ctor is member-wise and double delete will happen. ( If one does not forget to add D::~D)
  3. It can be fixed with rule of three.
  4. It can be more efficient with rule of five because a is never used after constructing d and it would be better if it could relinquish its buffer and be "consumed".
  5. 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.
  6. Writing five functions is tedious and replacing char* with std::vector gets us rule of zero, which is nice.
  7. 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.
  8. Now some students might ask about whether D requires 0/3/5 and how can we also fix that.
  9. 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 of dd.
  10. Also one can always confuse some people with D dd not calling the assignment operator.
  11. 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.

  • Related