Ziffusion
Ziffusion

Reputation: 8923

In C++, how can one predict if move or copy semantics would be invoked?

Given the latitude that a C++ compiler has in instantiating temporary objects, and in invoking mechanisms like return value optimization etc., it is not always clear by looking at some code if move or copy semantics will be invoked (or how many).

It almost feels as if these primitives exist for incidental optimizations. That is, you may or may not get them. It seems like it's difficult to design any kind of resource management strategy that leverages moves, when it is hard to control the invocation of moves themselves.

Is there a way to predict clearly (and simply) where and how many copies and moves might occur in some code? Ideally, one would not need to be an expert in compiler internals to be able to do this.

Upvotes: 2

Views: 77

Answers (1)

lubgr
lubgr

Reputation: 38287

It seems like it's difficult to design any kind of resource management strategy that leverages moves, when it is hard to control the invocation of moves themselves.

I would contradict here. Leveraging move semantics when designing a resource handling class should be done independently of how or when copy- or move-construction occurs in the client code. Once move-ctor/assignment is there, client code can be designed to leverage the existence of these special member functions.

Is there a way to predict clearly (and simply) where and how many copies and moves might occur in some code?

A bit hard to tell what simply means here, but this is how I understand it:

  • Given that a class has no move ctor/assignment operator, you will always get a copy. This is trivial, but important to keep in mind when working with e.g. classes in a legacy code that have user defined destructors and/or copy-ctor/assignment, because the compiler doesn't generate move ctors/assignment in this case.

  • Return value optimization. The question is tagged C++11, so you don't have guaranteed copy elision for initialization with prvalues brought by C++17. However, it is fair to assume that identical mechanism are already implemented by your compiler. Hence,

    struct A {};
    
    A func() { return A{}; }
    

    can be assumed to construct the instance of A to which the function return value is bound on the calling side in place. This causes neither move nor copy construction. The same behavior can optimistically be assumed if the returned object has a name, as long as func() has no branching that renders NRVO impossible.

    As an exception from this guideline, function return values that are also function parameters do not qualify for return value optimization. Hence, move/forward them to prevent copy in case A is move-constructible:

    A func(A& a) { return std::move(a); }
    

    The object created by the return value of func(A&) will hence be move-constructed.

  • Function parameters do not reveal per se how they behave, it depends on the type and its special member functions. Given

    void f1(A a1) { A a2{std::move(a1)}; };
    void f2(A& a1) { /* Same as above. */ };
    void f1(A&& a1) { /* Again, same. */ };
    

    the instances a2 are move-constructed if A has a move ctor, otherwise, it's copy.

There is a lot to discover beyond the exemplary cases above, I am neither capable of going into more detail, nor would this fit into the desired simplicity of an answer. Also, the scenario is different when you don't know the types you are dealing with, e.g. in function or class templates. In this case, a good read on how to deal with the related uncertainty of whether copies or moves are made is Item 29 in Eff. Modern C++ ("Assume that move operations are not present, not cheap, and not used").

Upvotes: 1

Related Questions