Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Return by value copies instead of moving

Tags:

c++

c++11

Why does this program call the copy constructor instead of the move constructor?

class Qwe {
public:
    int x=0;
    Qwe(int x) : x(x){}
    Qwe(const Qwe& q) {
        cout<<"copy ctor\n";
    }
    Qwe(Qwe&& q) {
        cout<<"move ctor\n";
    }    
};

Qwe foo(int x) {
    Qwe q=42;
    Qwe e=32;
    cout<<"return!!!\n";
    return q.x > x ? q : e;
}

int main(void)
{
    Qwe r = foo(50);
}

The result is:

return!!!
copy ctor

return q.x > x ? q : e; is used to disable nrvo. When I wrap it in std::move, it is indeed moved. But in "A Tour of C++" the author said that the move c'tor must be called when it available.

What have I done wrong?

like image 473
Art Avatar asked Oct 08 '17 07:10

Art


People also ask

Does return by value mean extra copies and extra overhead?

Maybe. In C++11, values are returned by moving, rather than copying, if they have a move constructor. This can be much more efficient than copying. In some circumstances - such as when returning a local variable or a temporary (as you do here) - the move or copy can be elided.

Is std:: move required?

std::move itself does "nothing" - it has zero side effects. It just signals to the compiler that the programmer doesn't care what happens to that object any more. i.e. it gives permission to other parts of the software to move from the object, but it doesn't require that it be moved.

Why is move better than copy?

If we are cutting(moving) within a same disk, then it will be faster than copying because only the file path is modified, actual data is on the disk. If the data is copied from one disk to another, it will be relatively faster than cutting because it is doing only COPY operation.

How does copy elision work?

Guaranteed copy elision redefines a number of C++ concepts, such that certain circumstances where copies/moves could be elided don't actually provoke a copy/move at all. The compiler isn't eliding a copy; the standard says that no such copying could ever happen.


2 Answers

You did not write your function in a way that allows copy/move elision to occur. The requirements for a copy to be replaced by a move are as follows:

[class.copy.elision]/3:

In the following copy-initialization contexts, a move operation might be used instead of a copy operation:

  • If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression

overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If the first overload resolution fails or was not performed, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

The above is from C++17, but the C++11 wording is pretty much the same. The conditional operator is not an id-expression that names an object in the scope of the function.

An id-expression would be something like q or e in your particular case. You need to name an object in that scope. A conditional expression doesn't qualify as naming an object, so it must preform a copy.


If you want to exercise your English comprehension abilities on a difficult wall of text, then this is how it's written in C++11. Takes some effort to see IMO, but it's the same as the clarified version above:

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. [...] This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

  • in a return statement in a function with a class return type, when the expression is the name of a non-volatile automatic object (other than a function or catch-clause parameter) with the same cv-unqualified type as the function return type, the copy/move operation can be omitted by constructing the automatic object directly into the function's return value

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object's type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.

like image 141
StoryTeller - Unslander Monica Avatar answered Oct 05 '22 16:10

StoryTeller - Unslander Monica


StoryTeller didn't answer the question: Why is the move c'tor not called? (And not: Why is there no copy elision?)

Here's my go: The move c'tor will be called if and only if:

  • Copy elision (RVO) is not performed. Your use of the ternary operator is indeed a way to prevent copy elision. Let me point out though that return (0, q); is a simpler way to do this if you just want to return q while suppressing copy elision. This uses the (in-)famous comma operator. Possibly return ((q)); might work, too, but I am not enough of a language lawyer to tell for sure.
  • The argument to return is an rvalue. This could be a temporary (more precisely, a prvalue), but these are also eligible for copy elision. Therefore, the argument to return must be an xvalue, such as std::move(q) if you want to ensure the move c'tor is called.

See also: C++ value categories

Some more technicalities of your example:

  • q and e are objects of type Qwe.
  • q.x > x ? q : e is an lvalue expression of type Qwe. This is because the expressions q and e are lvalues of type Qwe. The ternary operator just selects either of them.
  • std::move(q.x > x ? q : e) is an xvalue expression of type Qwe. The std::move simply turns (casts) the lvalue into an xvalue. As an aside, q.x > x ? std::move(q) : std::move(e) would also work.
  • The copy c'tor gets called in return q.x > x ? q : e; because it can be called with an lvalue of type Qwe (constness is optional), while, on the other hand, the move c'tor cannot be called with an lvalue and is therefore eliminated from the candidate set.

UPDATE: Addressing the comments by going into more depth… this is a really confusing aspect of C++!

Conceptually, in C++98, returning an object by value meant returning a copy of the object, so the copy c'tor would be called. However, the standard's authors considered that a compiler should be free to perform an optimization such that this potentially expensive copy (e.g. of a container) could be elided under suitable circumstances.

This copy elision means that, instead of creating the object in one place and then copying it to a memory address controlled by the caller, the callee creates the object directly in the memory controlled by the caller. Therefore, only the "normal" constructor, e.g. a default c'tor, is called.

Therefore, they added a passage such that the compiler is required to check that the copy c'tor — whether generated or user-defined – exists and is accessible (there was no notion yet of deleted functions for that matter), and must ensure that the object is initialized as-if it had been first created in a different place and then copied (cf. as-if rule), but the compiler was not required to ensure that any side effects of the copy c'tor would be observable, such as the stream output in your example.

The reason why the c'tor was still required to be there was that they wanted to avoid a scenario where a compiler was able to accept code that another would have to reject, simply because the former implemented an optional optimization that the latter did not.

In C++11, move semantics were added, and the committee very much wanted to use this in such a manner that a lot of existing return-by-value functions e.g. involving strings or containers would become more efficient. This was done in such a way that conditions were given under which the compiler was actually required to perform a move instead of a copy. However, the idea of copy elision remained important, so basically there were now four different categories:

  1. The compiler is required to check for a usable (see above) move c'tor, but is allowed to elide it.
  2. The compiler is required to check for a usable move c'tor, and has to call it.
  3. The compiler is required to check for a usable copy c'tor, but is allowed to elide it.
  4. The compiler is required to check for a usable copy c'tor, and has to call it.

… which in turn lead to four possible outcomes:

  1. Compiler checks for move c'tor, but then elides it. (relates to 1. above)
  2. Compiler checks for move c'tor and actually emits a call to it. (relates to 1. or 2. above)
  3. Compiler checks for copy c'tor, but then elides it. (relates to 3. above)
  4. Compiler checks for copy c'tor and actually emits a call to it. (relates to 3. or 4. above)

And the long optimization story doesn't end here, because, in C++17, the compiler is required to elide certain c'tor calls. In these cases, the compiler is not even allowed to demand that a copy or move c'tor is available.

Note that a compiler has always been free to elide even such c'tor calls that do not meet the standard requirements, under the protection of the as-if rule, for instance by function inlining and the following optimization steps. Anyway, a function call, conceptually, does not have to be backed by the actual machine instruction for the execution of a subroutine. The compiler is just not allowed to remove observable, otherwise defined behavior.

By now you should have noticed that, at least prior to C++17, it is very well possible for the same well-formed program to behave differently, depending on the compiler used and even optimization settings, if the copy rsp. move constructor has observable side effects. Also, a compiler that implements copy/move elision may do so for a subset of the conditions under which the standard allows it to happen. This makes your question almost impossible to answer in detail. Why is the copy/move c'tor called here, but not there? Well, it may be because of the requirements of the C++ standard, but it also may be the preference of your compiler. Maybe the compiler authors had time and leisure implementing the one optimization but not the other. Maybe they found it too difficult in the latter case. Maybe they just had more important stuff to do. Who knows?

What matters 99% of the time for me as a developer is to write my code in such a way that the compiler can apply the best optimizations. Sticking to common cases and standard practice is one thing. Knowing NRVO and RVO of temporaries is another thing, and writing the code such that the standard allows (or, in C++17, requires) copy/move elision, and ensuring that a move c'tor is available where it is beneficial (in case elision does not occur). Don't rely on side effects such as writing a log message or incrementing a global counter. These are not what a copy or move c'tor should commonly do anyway, except possibly for debugging or scholarly interest.

like image 37
Arne Vogel Avatar answered Oct 05 '22 16:10

Arne Vogel