Looking at below code sample I would expect it to perform mandatory copy elision as part of Return Value Optimization (RVO) and compile with C++17 (/std:c++17) but it compiles with an error on Visual Studio 2017 (I'm using VS17, 15.9.8 more specifically).
class NoCopyOrMove
{
public:
NoCopyOrMove() = default;
NoCopyOrMove(int a, int b){}
NoCopyOrMove(const NoCopyOrMove&) = delete;
NoCopyOrMove& operator=(const NoCopyOrMove&) = delete;
NoCopyOrMove(NoCopyOrMove&&) = delete;
NoCopyOrMove& operator=(NoCopyOrMove&&) = delete;
private:
int a, b;
};
NoCopyOrMove get(bool b)
{
return b ? NoCopyOrMove(1,2) : NoCopyOrMove();
//if (b)
// return NoCopyOrMove(1, 2);
//return NoCopyOrMove();
}
int main()
{
NoCopyOrMove m = get(true);
}
Error is:
error C2280: 'NoCopyOrMove::NoCopyOrMove(NoCopyOrMove &&)': attempting to reference a deleted function
note: see declaration of 'NoCopyOrMove::NoCopyOrMove'
note: 'NoCopyOrMove::NoCopyOrMove(NoCopyOrMove &&)': function was explicitly deleted
NOTE: seems to compile on GCC and the the version with if/else compiles fine on both so not sure what I am missing.
I found a few other questions on stackoverflow, but they were from pre-c17 era and were mostly referring to "copy is called instead of move", hence asking again.
based on cppreference Copy elision happens:
In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:
and result of ternary operator should be a prvalue:
a ? b : c, the ternary conditional expression for some b and c (see definition for detail);
Any ideas why it doesn't compile?
Edit to use a simpler code:
given the NoCopyOrMove above, below code is also attempting to call move-ctor.
int main()
{
volatile bool b = true;
NoCopyOrMove m = b ? NoCopyOrMove(1,2) : NoCopyOrMove();
}
Update: report link
Yes. This is a bug in MSVC. Pretty much every other compiler that supports C++17 compiles it. Below we have the assembly produced by:
And all of them compile it with -std=c++17
or -std=c++1z
(for ellcc).
Conditional expressions (the ones formed by the ternary operator) produce values according to these rules (see section 8.5.16).
Paragraph 1 of 8.5.16 describes sequencing, and parts 2 through 7 describe the value category of the resulting expression (see section 8.2.1 for a description of value categories).
Otherwise, the result is a prvalue. If the second and third operands do not have the same type, and either has (possibly cv-qualified) class type, overload resolution is used to determine the conversions (if any) to be applied to the operands (16.3.1.2, 16.6). If the overload resolution fails, the program is ill-formed. Otherwise, the conversions thus determined are applied, and the converted operands are used in place of the original operands for the remainder of this subclause.
This gives us our answer. The result is a prvalue, so it's unnecessary to use the copy or move constructors, as the value will be instantiated in the memory provided by the calling function (this memory location is passed as a "hidden" parameter to your function).
Jon Harper was kind enough to point out that the standard states:
A program that refers to a deleted function implicitly or explicitly, other than to declare it, is ill-formed. (11.4.3.2)
This begs the question: does your program implicitly refer to the move constructor or the copy constructor?
The answer to this is no. Because the result of the conditional expression is a prvalue, no temporary is materialized, and as a result neither the move constructor or the copy constructor is referenced, either explicitly or implicitly. To quote cppreference (emphasis mine):
Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to. The copy/move constructors need not be present or accessible, as the language rules ensure that no copy/move operation takes place, even conceptually:
In a return statement, when the operand is a prvalue of the same class type (ignoring cv-qualification) as the function return type:
T f() { return T(); }
f(); // only one call to default constructor of T
In the initialization of a variable, when the initializer expression is a prvalue of the same class type (ignoring cv-qualification) as the variable type:
T x = T(T(f())); // only one call to default constructor of T, to initialize x
One source of contention is whether or not Copy Elision is guaranteed. It is important to distinguish between Named Return Value Optimization, and pure Return Value Optimization.
If you return a local variable, it's not guaranteed. This is Named Return Value Optimization. If your return statement is an expression that's a prvalue, it is guaranteed.
For example:
NoCopyOrMove foo() {
NoCopyOrMove myVar{}; //Initialize
return myVar; //Error: Move constructor deleted
}
I am returning a an expression (myVar
) that is the name of an object of automatic storage. In this case, return value optimization is permitted but not guaranteed. Section 15.8.3 of the standard applies here.
On the other hand, if I write:
NoCopyOrMove foo() {
return NoCopyOrMove(); // No error (C++17 and above)
}
Copy Elision is guaranteed, and no copy or move takes place. Similarly, if I write:
NoCopyOrMove foo(); //declare foo
NoCopyOrMove bar() {
return foo(); //Returns what foo returns
}
Copy Elision is still guaranteed because the result of foo()
is a prvalue.
MSVC does, in fact, have a bug.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With