Copy ctor called instead of move ctor
Asked Answered
C

4

5

Why is the copy constructor called when returning from bar instead of the move constructor?

#include <iostream>

using namespace std;

class Alpha {
public:
  Alpha() { cout << "ctor" << endl; }
  Alpha(Alpha &) { cout << "copy ctor" << endl; }
  Alpha(Alpha &&) { cout << "move ctor" << endl; }
  Alpha &operator=(Alpha &) { cout << "copy asgn op" << endl; }
  Alpha &operator=(Alpha &&) { cout << "move asgn op" << endl; }
};

Alpha foo(Alpha a) {
  return a; // Move ctor is called (expected).
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor is called (unexpected).
}

int main() {
  Alpha a, b;
  a = foo(a);
  a = foo(Alpha());
  a = bar(Alpha());
  b = a;
  return 0;
}

If bar does return move(a) then the behavior is as expected. I do not understand why a call to std::move is necessary given that foo calls the move constructor when returning.

Ceria answered 22/8, 2017 at 2:9 Comment(8)
answered in here but not sure if it is a dupe.Gmt
@Gmt that post provided the insight I needed. Follow-up answer to be provided shortly.Ceria
In foo, the object designated by a is local to the function, so it's guaranteed that the object expires and is safe to move from. In bar, nothing is known about the object designated by a`, so you don't want to modify it silently. Remember that rvalue references are just another kind of reference; the core language has no opinion on what you use it for, nor does it have any expectations on lifetime or aliases.Ungrounded
@KerrekSB Is moving a in foo an implementation behavior? I failed to find any relevant description about that behavior in the standard.Uranalysis
@Uranalysis it's part of the standard. If RVO does not occur, then a copy or move constructor is called on function return. Since I defined a move constructor and a is guaranteed to expire once foo is out of scope, the move constructor is called.Ceria
@JacobPollack As a named parameter, a (in foo) is an lvalue, then copy constructor should be called; that's all what I found in the standard. I can't find any quotes about that if a is to be expired then move constructor could be used instead. That's why I'm guessing that this is an implementation behavior, not guaranteed by the standard.Uranalysis
@Uranalysis see N4296.12.8.32.Ceria
@JacobPollack: "moving" isn't a real thing. That's just a colloquial shorthand for certain kinds of common implementation strategies. As far as the language is concerned, there's only the binding of values to references.Ungrounded
C
4

There are 2 things to understand in this situation:

  1. a in bar(Alpha &&a) is a named rvalue reference; therefore, treated as an lvalue.
  2. a is still a reference.

Part 1

Since a in bar(Alpha &&a) is a named rvalue reference, its treated as an lvalue. The motivation behind treating named rvalue references as lvalues is to provide safety. Consider the following,

Alpha bar(Alpha &&a) {
  baz(a);
  qux(a);
  return a;
}

If baz(a) considered a as an rvalue then it is free to call the move constructor and qux(a) may be invalid. The standard avoids this problem by treating named rvalue references as lvalues.

Part 2

Since a is still a reference (and may refer to an object outside of the scope of bar), bar calls the copy constructor when returning. The motivation behind this behavior is to provide safety.

References

  1. SO Q&A - return by rvalue reference
  2. Comment by Kerrek SB
Ceria answered 22/8, 2017 at 2:49 Comment(2)
This answer is extremely misleading because it does not make the absolutely critical distinction between type and value category.Tableware
IMO it's not very insightful to say "a named rvalue reference is an lvalue". Instead, distinguish between entities and expressions. The entity a has type Alpha&&, but the expression a is an lvalue of type Alpha. Expressions never have reference type, it's meaningless at best to talk about a being a reference in the context of talking about how it behaves in an expression.Breakage
C
1

yeah, very confusing. I would like to cite another SO post here implicite move. where I find the following comments a bit convincing,

And therefore, the standards committee decided that you had to be explicit about the move for any named variable, regardless of its reference type

Actually "&&" is already indicating let-go and at the time when you do "return", it is safe enough to do move.

probably it is just the choice from standard committee.

item 25 of "effective modern c++" by scott meyers, also summarized this, without giving much explanations.

Alpha foo() {
  Alpha a
  return a; // RVO by decent compiler
}
Alpha foo(Alpha a) {
  return a; // implicit std::move by compiler
}

Alpha bar(Alpha &&a) {
  return a; // Copy ctor due to lvalue
}

Alpha bar(Alpha &&a) {
  return std:move(a); // has to be explicit by developer
}
Casto answered 22/8, 2017 at 20:32 Comment(0)
T
1

This is a very very common mistake to make as people first learn about rvalue references. The basic problem is a confusion between type and value category.

int is a type. int& is a different type. int&& is yet another type. These are all different types.

lvalues and rvalues are things called value categories. Please check out the fantastic chart here: What are rvalues, lvalues, xvalues, glvalues, and prvalues?. You can see that in addition to lvalues and rvalues, we also have prvalues and glvalues and xvalues, and they form a various venn diagram sort of relation.

C++ has rules that say that variables of various types can bind to expressions. An expressions reference type however, is discarded (people often say that expressions do not have reference type). Instead, the expression have a value category, which determines which variables can bind to it.

Put another way: rvalue references and lvalue references are only directly relevant on the left hand of the assignment, the variable being created/bound. On the right side, we are talking about expressions and not variables, and rvalue/lvalue reference-ness is only relevant in the context of determining value category.

A very simple example to start with is simple looking at things of purely type int. A variable of type int as an expression, is an lvalue. However, an expression consisting of evaluating a function that returns an int, is an rvalue. This makes intuitive sense to most people; the key thing though is to separate out the type of an expression (even before references are discarded) and its value category.

What this is leading to, is that even though variables of type int&& can only bind to rvalues, does not mean that all expressions with type int&&, are rvalues. In fact, as the rules at http://en.cppreference.com/w/cpp/language/value_category say, any expression consisting of naming a variable, is always an lvalue, no matter the type.

That's why you need std::move in order to pass along rvalue references into subsequent functions that take by rvalue reference. It's because rvalue references do not bind to other rvalue references. They bind to rvalues. If you want to get the move constructor, you need to give it an rvalue to bind to, and a named rvalue reference is not an rvalue.

std::move is a function that returns an rvalue reference. And what's the value category of such an expression? An rvalue? Nope. It's an xvalue. Which is basically an rvalue, with some additional properties.

Tableware answered 22/8, 2017 at 22:18 Comment(0)
B
1

In both foo and bar, the expression a is an lvalue. The statement return a; means to initialize the return value object from the initializer a, and return that object.

The difference between the two cases is that overload resolution for this initialization is performed differently depending on whether or not a declared as a non-volatile automatic object within the innermost enclosing block, or a function parameter.

Which it is for foo but not bar. (In bar , a is declared as a reference). So return a; in foo selects the move constructor to initialize the return value, but return a; in bar selects the copy constructor.

The full text is C++14 [class.copy]/32:

When the criteria for elision of a copy/move operation are met, but not for an exception-declaration , and the object to be copied is designated by an lvalue, or when 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. [Note: This two-stage overload resolution must be performed regardless of whether copy elision will occur. It determines the constructor to be called if elision is not performed, and the selected constructor must be accessible even if the call is elided. —end note ]

where "criteria for elision of a copy/move operation are met" refers to [class.copy]/31.1:

  • 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

Note, these texts will change for C++17.

Breakage answered 22/8, 2017 at 23:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.