18

I recently have been reading about move constructors in C++ (see e.g. here) and I am trying to understand how they work and when I should use them.

As far as I understand, a move constructor is used to alleviate the performance problems caused by copying large objects. The wikipedia page says: "A chronic performance problem with C++03 is the costly and unnecessary deep copies that can happen implicitly when objects are passed by value."

I normally address such situations

  • by passing the objects by reference, or
  • by using smart pointers (e.g. boost::shared_ptr) to pass around the object (the smart pointers get copied instead of the object).

What are the situations in which the above two techniques are not sufficient and using a move constructor is more convenient?

1
  • 1
    Besides the fact that move semantics can achieve much more (as said in the answers), you shouldn't ask what are the situations where passing by reference or by smart pointer are not sufficient, but if those techniques are really the best and cleanest way to do so (god beware a shared_ptr just for the sake of fast copying) and if move semantics can achieve the same with nearly no coding-, semantics- and cleanliness-penalty. Jun 1, 2012 at 9:27

4 Answers 4

16

Move semantics introduce a whole dimension to C++ - it isn't just there to let you return values cheaply.

For example, without move-semantics std::unique_ptr doesn't work - look at std::auto_ptr, which was deprecated with the introduction of move-semantics and removed in C++17. Moving a resource is vastly different from copying it. It allows transfer of ownership of a unique item.

For example, let's not look at std::unique_ptr, since it is fairly well discussed. Let's look at, say, a Vertex Buffer Object in OpenGL. A vertex buffer represents memory on the GPU - it needs to be allocated and deallocated using special functions, possibly having tight constraints on how long it can live. It is also important that only one owner use it.

class vertex_buffer_object
{
    vertex_buffer_object(size_t size)
    {
        this->vbo_handle = create_buffer(..., size);
    }

    ~vertex_buffer_object()
    {
        release_buffer(vbo_handle);
    }
};

void create_and_use()
{
    vertex_buffer_object vbo = vertex_buffer_object(SIZE);

    do_init(vbo); //send reference, do not transfer ownership

    renderer.add(std::move(vbo)); //transfer ownership to renderer
}

Now, this could be done with a std::shared_ptr - but this resource is not to be shared. This makes it confusing to use a shared pointer. You could use std::unique_ptr, but that still requires move semantics.

Obviously, I haven't implemented a move constructor, but you get the idea.

The relevant thing here is that some resources aren't copyable. You can pass around pointers instead of moving, but unless you use unique_ptr, there is the issue of ownership. It is worthwhile to be as clear as possible as to what the intent of the code is, so a move-constructor is probably the best approach.

8
  • Thanks for the answer. What would happen if one used a shared pointer here?
    – Giorgio
    May 31, 2012 at 19:02
  • I try to answer myself: using a shared pointer would not allow to control the lifetime of the object, while it is a requirement that the object can only live for a certain amount of time.
    – Giorgio
    May 31, 2012 at 19:25
  • 3
    @Giorgio You could use a shared pointer, but it would be semantically wrong. It isn't possible to share a buffer. Also, that would essentially make you pass a pointer to a pointer (since the vbo is basically a unique pointer to GPU memory). Someone viewing your code later might wonder 'Why is there a shared pointer here? Is it a shared resource? That might be a bug!'. It's better to be as clear as possible as to what the original intent was.
    – Max
    May 31, 2012 at 19:25
  • @Giorgio Yes, that is also part of the requirement. When the 'renderer' in this case wants to deallocate some resource (possibly not enough memory for new objects on the GPU), there must not be any other handle to the memory. Using a shared_ptr that passes out of scope would work if you don't keep it around anywhere else, but why not make it completely obvious when you can?
    – Max
    May 31, 2012 at 19:28
  • @Giorgio See my edit for another try at clarifying.
    – Max
    May 31, 2012 at 19:32
5

Move semantics aren't necessarily all that big of an improvement when you're returning a value -- and when/if you use a shared_ptr (or something similar) you're probably prematurely pessimizing. In reality, nearly all reasonably modern compilers do what's called Return Value Optimization (RVO) and Named Return Value Optimization (NRVO). This means that when you're returning a value, instead of actually copying the value at all, they simply pass a hidden pointer/reference to where the value's going to be assigned after the return, and the function uses that to create the value where it's going to end up. The C++ standard includes special provisions to allow this, so even if (for example) your copy constructor has visible side effects, it's not required to use the copy constructor to return the value. For example:

#include <vector>
#include <numeric>
#include <iostream>
#include <stdlib.h>
#include <algorithm>
#include <iterator>

class X {
    std::vector<int> a;
public:
    X() {
        std::generate_n(std::back_inserter(a), 32767, ::rand);
    }

    X(X const &x) {
        a = x.a;
        std::cout << "Copy ctor invoked\n";
    }

    int sum() { return std::accumulate(a.begin(), a.end(), 0); }
};

X func() {
    return X();
}

int main() {
    X x = func();

    std::cout << "sum = " << x.sum();
    return 0;
};

The basic idea here is fairly simple: create a class with enough content we'd rather avoid copying it, if possible (the std::vector we fill with 32767 random ints). We have an explicit copy ctor that'll show us when/if it gets copied. We also have a little more code to do something with the random values in the object, so the optimizer won't (at least easily) eliminate everything about the class just because it does nothing.

We then have some code to return one of these objects from a function, and then use the summing to ensure the object is really created, not just ignored completely. When we run it, at least with most recent/modern compilers, we find that the copy constructor we wrote never runs at all -- and yes, I'm pretty sure that even a fast copy with a shared_ptr is still slower than doing no copying at all.

Moving allows you to do a fair number of things you simply couldn't do (directly) without them. Consider the "merge" part of an external merge sort -- you have, say, 8 files you're going to merge together. Ideally you'd like to put all 8 of those files into a vector -- but since vector (as of C++03) needs to be able to copy elements, and ifstreams can't be copied, you're stuck with some unique_ptr/shared_ptr, or something on that order to be able to put them in a vector. Note that even if (for example) we reserve space in the vector so we're sure our ifstreams will never really be copied, the compiler won't know that, so the code won't compile even though we know the copy constructor will never be used anyway.

Even though it still can't be copied, in C++11 an ifstream can be moved. In this case, the objects probably won't ever be moved, but the fact that they could be if necessary keeps the compiler happy, so we can put our ifstream objects in a vector directly, without any smart pointer hacks.

A vector that does expand is a pretty decent example of a time that move semantics really can be/are useful though. In this case, RVO/NRVO won't help, because we're not dealing with the return value from a function (or anything very similar). We have one vector holding some objects, and we want to move those objects into a new, larger chunk of memory.

In C++03, that was done by creating copies of the objects in the new memory, then destroying the old objects in the old memory. Making all those copies just to throw away the old ones, however, was quite a waste of time. In C++11, you can expect them to be moved instead. This typically lets us, in essence, do a shallow copy instead of a (generally much slower) deep copy. In other words, with a string or vector (for only a couple of examples) we just copy the pointer(s) in the objects, instead of making copies of all the data those pointers refer to.

2
  • Thanks for the very detailed explanation. If I understand correctly, all the situations in which moving comes into play could be handled by normal pointers, but it would be unsafe (complex and error prone) to program all the pointer juggling each time. So, instead, there is some unique_ptr (or similar mechanism) under the hood and the move semantics ensures that at the end of the day there is only some pointer copying and no object copying.
    – Giorgio
    Jun 1, 2012 at 5:15
  • @Giorgio: Yes, that's pretty much correct. The language doesn't really add move semantics; it adds rvalue references. An rvalue reference (obviously enough) can bind to an rvalue, in which case you know it's safe to "steal" its internal representation of the data and just copy its pointers instead of doing a deep copy. Jun 1, 2012 at 5:17
4

Consider:

vector<string> v;

When adding strings to v, it will expand as needed, and on each reallocation the strings will have to be copied. With move constructors, this is basically a non-issue.

Of course, you could also do something like:

vector<unique_ptr<string>> v;

But that will work well only because std::unique_ptr implements move constructor.

Using std::shared_ptr makes sense only in (rare) situations when you actually have shared ownership.

1
  • but what if instead of string we had an instance of Foo where it has 30 data members? The unique_ptr version wouldn't be more efficient?
    – Vassilis
    Dec 29, 2018 at 9:34
2

Return values are where I'd most often like to pass by value instead of some kind of reference. Being able to quickly return an object 'on the stack' without a massive performance penalty would be nice. On the other hand, it's not particularly difficult to get around this (shared pointers are just so easy to use...), so I'm not sure it's really worth doing extra work on my objects just to be able to do this.

3
  • I also normally use smart pointers to wrap objects that are returned from a function / method.
    – Giorgio
    May 31, 2012 at 18:39
  • 1
    @Giorgio: That's definitely both obfuscating and slow.
    – DeadMG
    May 31, 2012 at 21:40
  • Modern compilers should perform an automatic move if you return a simple on-the-stack object, so there is no need for shared ptrs etc. Aug 25, 2016 at 9:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.