std::optional: Not participating in overload resolution vs. being defined as deleted - c++

I am trying to understand the mechanism behind type traits propagation as described for std::optional in http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0602r4.html. There is a subtle difference in the treatment of copy operations, which shall be conditionally defined as deleted, versus move operations, which shall rather not participate in overload resolution.
What is the reason for that difference, and how would I test the latter? Example:
#include <type_traits>
#include <optional>
struct NonMoveable {
NonMoveable() = default;
NonMoveable(NonMoveable const&) = default;
NonMoveable(NonMoveable&&) = delete;
NonMoveable& operator=(NonMoveable const&) = default;
NonMoveable& operator=(NonMoveable&&) = delete;
};
// Inner traits as expected
static_assert(!std::is_move_constructible<NonMoveable>::value);
static_assert(!std::is_move_assignable<NonMoveable>::value);
// The wrapper is moveable, via copy operations participating in
// overload resolution. How to verify that the move operations don't?
static_assert(std::is_move_constructible<std::optional<NonMoveable>>::value);
static_assert(std::is_move_assignable<std::optional<NonMoveable>>::value);
int main(int argc, char* argv[])
{
NonMoveable a1;
NonMoveable a2{std::move(a1)}; // Bad, as expected
std::optional<NonMoveable> b1;
std::optional<NonMoveable> b2{std::move(b1)}; // Good, see above. But
// useless as a test for
// P0602R4.
return 0;
}
Bonus Question
Does GCC do the right thing? I have modified the example a bit to get a tiny step closer: https://godbolt.org/z/br1vx1. Here I made the copy operations inaccessible by declaring them private. GCC-10.2 with -std=c++20 now fails the static asserts and complains
error: use of deleted function 'std::optional<NonMoveable>::optional(std::optional<NonMoveable>&&)'
According to Why do C++11-deleted functions participate in overload resolution? the delete is applied after overload resolution, which could indicate that the move constructor participated, despite P0602R4 said it shall not.
On the other hand https://en.cppreference.com/w/cpp/language/overload_resolution states right in the beginning
... If these steps produce more than one candidate function, then overload resolution is performed ...
so overload resolution was skipped, because the move constructor was the only candidate?

std::optional is a red herring; the key is understanding the mechanisms that lead to why these requirements are placed on a library type
There is a subtle difference in the treatment of copy operations, which shall be conditionally defined as deleted, versus move operations, which shall rather not participate in overload resolution.
The under-the-hood requirements (and how to implement these) for std::optional are complex. However, the requirement that move operations shall not participate in overload resolution (for non-movable types) vs copy operations being deleted (for non-copyable types) likely relates to a separate topic;
NRVO(1) (a case of copy elision, if you may) and
more implicit moves, e.g. choosing move constructors over copy constructors when returning named objects with automatic storage duration from a function.
We can understand this topic by looking at simpler types than std::optional.
(1) Named Returned Value Optimization
TLDR
The move eagerness that is expanding in C++ (more eager moves in C++20) means there are special cases where a move constructor will be chosen over a copy constructor even if the move constructor has been deleted. The only way to avoid these for, say, non-movable types, is to make sure the type has no move constructor nor a move assignment operator, by knowledge of the rule of 5 and what governs whether these are defined implicitly.
The same preference does not exist for copying, and there would be no reasons to favour removing these over deleting them, if this was even possible (2). In other words, the same kinks that exist for favouring moves in overload resolution, that sometimes unexpectedly choose move over copy, is not present for the reverse; copy over move.
(2) There is no such thing as a class without the existence of a copy ctor and a copy assignment operator (although these may be defined as deleted).
Implicit move eagerness
Consider the following types:
struct A {
A() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
A(A const &) { std::cout << __PRETTY_FUNCTION__ << "\n"; }
A &operator=(A const &) {
std::cout << __PRETTY_FUNCTION__ << "\n";
return *this;
}
};
struct B {
B() { std::cout << __PRETTY_FUNCTION__ << "\n"; }
B(B const &) { std::cout << __PRETTY_FUNCTION__ << "\n"; }
B &operator=(B const &) {
std::cout << __PRETTY_FUNCTION__ << "\n";
return *this;
}
B(B &&) = delete;
B &operator=(B &&) = delete;
};
Where, A:
has user-defined constructors, such that a move constructor and move assigment operator will not be implicitly defined,
and where B, moreover:
declares and deletes a move constructor and a move assignment operator; as these are declared and defined as deleted, they will participate in overload resolution.
Before we continue through different standard versions, we define the following functions that we shall return to:
A getA() {
A a{};
return a;
}
B getB() {
B b{};
return b;
}
C++14
Now, in C++14 an implementation was allowed to implement copy(/move) elision for certain scenarios; citing [class.copy]/31 from N4140 (C++14 + editorial fixes) [emphasis mine]:
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the constructor selected for the copy/move operation and/or the destructor for the object have side effects. [...]
This elision of copy/move operations, called copy elision, is permitted in the following circumstances:
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
[...]
and, from [class.copy]/32 [emphasis mine]:
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.
But [class.temporary]/1 still placed the same semantic restrictions on an elided copy of an object as if the copy had actually not been elided [emphasis mine]
[..] Even when the creation of the temporary object is unevaluated (Clause [expr]) or otherwise avoided ([class.copy]), all the semantic restrictions shall be respected as if the temporary object had been created and later destroyed.
Such that, even for a situation where copy elision was eligible (and performed), the conversion sequences from, say, a named object viable for NRVO, would need to go through overload resolution to find (possibly elided) converting constructors, and would start with a pass through overload resolution as if the object were designated by an rvalue. This means, that in C++14, the following was well-formed
auto aa{getA()}; // OK, and copy most likely elided.
whereas the following was ill-formed:
auto bb{getB()}; // error: use of deleted function 'B::B(B&&)'
as overload resolution would find the declared but deleted move constructor of B during the step of considering b in return b; in getB() as an rvalue. For A, no move constructor exists, meaning overload resolution for a in return a; in getA() with a as an rvalue would fail, and whereafter overload resolution without this kink would succeed in finding the copy constructor of A (which would subsequently be elided).
C++17
Now, in C++17 copy elision was made stronger by the concept of delayed (end entirely elided) materialization of temporaries, particularly adding [class.temporary]/3 [emphasis mine]:
When an object of class type X is passed to or returned from a function, if each copy constructor, move constructor, and destructor of X is either trivial or deleted, and X has at least one non-deleted copy or move constructor, implementations are permitted to create a temporary object to hold the function parameter or result object. The temporary object is constructed from the function argument or return value, respectively, and the function's parameter or return object is initialized as if by using the non-deleted trivial constructor to copy the temporary (even if that constructor is inaccessible or would not be selected by overload resolution to perform a copy or move of the object).
This makes a large difference, as copy elision can now be performed for getB() without passing through the special rules of return value overload resolution (which previously picked the deleted move constructor), such that both of these are well-formed in C++17:
auto aa(getA()); // OK, copy elided.
auto bb(getB()); // OK, copy elided.
C++20
C++20 implements P1825R0 which allows even more implicit moves, expanding the cases where move construction or assignment may take place even when one would, at first glance, expect a copy construction/assignment (possible elided).
Summary
The quite complex rules with regard to move eagerness (over copying) can have some unexpected effects, and if a designer wants to make sure a type will not run into a corner case where a deleted move constructor or move assignment operator takes precedence in overload resolution over an non-deleted copy constructor or copy assignment operator, it is better to make sure that there are no move ctor/assignment operator available for overload resolution to find (for these cases), as compared to declaring them and defining them as explicitly-deleted. This argument does not apply for the move ctor/copy assignment operator however, as:
the standard contains no similar copy-eagerness (over move), and
there is no such thing as a class without a copy constructor or copy assignment operator, and removing these from overload resolution is basically(3) only possible in C++20 using requires-clauses.
As an example (and probably a GCC regression bug) of the difficulty of getting these rules right for a non-language lawyer, GCC trunk currently rejects the following program for C++20 (DEMO):
// B as above
B getB() {
B b{};
return b;
}
with the error message
error: use of deleted function 'B::B(B&&)'
In this case, one would expect a copy (possibly elided) to be chosen above in case B had deleted its move ctor. When in doubt, make sure the move ctor and assignment operator don't participate (i.e., exist) in overload resolution.
(3) One could declare a deleted assignment operator overloaded with both const- and ref-qualifiers, say const A& operator=(const A&) const && = delete;, which would very seldom be a viable candidate during overload solution (assignment to const rvalue), and which would guarantee the non-existence of the other non-const and &-qualified overloads that would otherwise likely to be valid overload candidates.

Related

Is copy/move elision allowed to make a program using deleted functions well-formed?

Consider the following code:
#include <iostream>
struct Thing
{
Thing(void) {std::cout << __PRETTY_FUNCTION__ << std::endl;}
Thing(Thing const &) = delete;
Thing(Thing &&) = delete;
Thing & operator =(Thing const &) = delete;
Thing & operator =(Thing &&) = delete;
};
int main()
{
Thing thing{Thing{}};
}
I expect Thing thing{Thing{}}; statement to mean construction of temporary object of Thing class using default constructor and construction of thing object of Thing class using move constructor with just created temporary object as an argument. And I expect that this program to be considered ill-formed because it contains an invocation of deleted move constructor, even though it can be potentially elided. The class.copy.elision section of standard seems to demand this as well:
the selected constructor must be accessible even if the call is elided
The Wording for guaranteed copy elision through simplified value categories does not seem to allow it either.
However gcc 7.2 (and clang 4 as well, but not VS2017 which still does not support guaranteed copy elision) will compile this code just fine eliding move constructor call.
Which behavior would be correct in this case?
It doesn't make an ill-formed program build. It gets rid of the reference to the deleted function entirely. The appropriate wording in the proposal is here:
[dcl.init] bullet 17.6
If the initializer expression is a prvalue and the cv-unqualified
version of the source type is the same class as the class of the
destination, the initializer expression is used to initialize the
destination object. [ Example: T x = T(T(T())); calls the T default
constructor to initialize x. ]
The example further strengthens this. Since it indicates the whole expression must collapse into a single default construction.
The thing to note is that the deleted function is never odr-used when the copies are elided due to value categories, so the program is not referring to it.
This is an important distinction, since the other form of copy elision still odr-uses the copy c'tor, as described here:
[basic.def.odr]/3
... A constructor selected to copy or move an object of class type is
odr-used even if the call is actually elided by the implementation
([class.copy] ...
[class.copy] describes the other form of permissible (but not mandatory) copy-elision. Which, if we demonstrate with your class:
Thing foo() {
Thing t;
return t; // Can be elided according to [class.copy.elision] still odr-used
}
Should make the program ill-formed. And GCC complains about it as expected.
And by the way. If you think the previous example in the online compiler is a magicians trick, and GCC complains because it needs to call the move c'tor. Have a look at what happens when we supply a definition.

Returning an fstream

I have this function:
fstream open_user_file() const
{
...
}
but my compiler complains about fstream copy-constructor being implicitly deleted. Given that the compiler performs RVO, why is the copy constructor chosen instead of the move constructor?
Otherwise, what's the best way to do this?
The currently accepted answer is just wrong.
When returning a local variable with automatic storage, of the same type as the declared return type of the function, then there is a two phase process going on:
fstream open_user_file() const
{
fstream f;
/*...*/
return f;
}
The selection of 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.
This means that if f is move constructible, that will be preferred (and possibly elided) for returning f. Else if f is copy constructible that will be done (and possibly elided) for returning f. Otherwise f can not be returned from this function and a compile-time error should result.
The only case in which:
return std::move(f);
should help is when the implementation is buggy. In a conforming implementation, fstream is move constructible and:
return f;
will be optimal. If f is not move constructible, then:
return std::move(f);
won't help in a conforming implementation. And if coded anyway in a conforming implementation will have the effect of a pessimization, in that it will inhibit RVO.
gcc 4.8 has not implemented movable streams (and streams are not copyable). And this is the source of your problem. In C++98, C++03, and gcc 4.8, streams are not returnable from functions. In C++11 they are.
An implementation may omit a copy operation resulting from a return statement, even if the copy constructor has side effects. In this case you may just have to explicitly move.
fstream open_user_file() const
{
fstream f;
/*...*/
return std::move(f);
}
When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class
object, even if the constructor selected for the copy/move operation and/or the destructor for the object
have side effects.
...
And this is where it says the copy constructor must be accessible:
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. [ 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.

Returning std::move of a local variable [duplicate]

This question already has answers here:
Using std::move() when returning a value from a function to avoid to copy
(3 answers)
Closed 9 years ago.
Let there be a class A with a move constructor. Consider this:
A get()
{
A a;
return std::move( a );
}
// later in the code
A aa = get();
Here the explicit call to std:move forces the move constructor of A to be called thus it might inhibit the return value optimization in while calling get(). Thus it is said the a better implementation of get() would be this:
A get()
{
A a;
return a;
}
But the return value optimization is not a part of C++11 standard, so WHAT IF the compiler, by some reason, decides not to perform return value optimization while calling get(). In this case a copy constructor of A will be called while returning in get() right?
So isn't the first implementation of get() more pereferible??
A compiler should use a move constructor, but I didn't see an obligation in the standard :
It's always said "Copy/move constructor" in the section concerning temporary objects
standard ISO/IEC 14882:2011 C++ :
"
12.1/9
A copy constructor (12.8) is used to copy objects of class type. A move constructor (12.8) is used to move
the contents of objects of class type.
12.8/32
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. [ 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 ]
"
lvalue = T &
rvalue = T &&
So, It says that first, the compiler will look if it find a move constructor, then, it will look for a move constructor.
Thus, if your compiler is conform to the standard, it will call the move constructor.
I append just that which is interesting:
"
12.8/31
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.
"
...So even if there is side effects in these constructors/destructors, they can be skipped

C++11 non-static member initializers and deleted copy constructor

I'm trying to compile the following simple code with GCC 4.7.2 (MinGW). Here I'm using C++11 feature - non-static member initializers:
#include <iostream>
using namespace std;
struct A
{
int var;
A()
{
cout << "A()\n";
}
A(int i)
{
cout << "A(int i)\n";
var = i;
}
A(const A&) = delete;
};
struct B
{
A a = 7;
};
int main()
{
B b;
cout << "b.a.var = " << b.a.var;
return 0;
}
This code doesn't compile because of deleted copy-constructor which isn't necessary here. Here are errors:
main.cpp:27:11: error: use of deleted function 'A::A(const A&)'
main.cpp:13:5: error: declared here
main.cpp: In constructor 'constexpr B::B()':
main.cpp:25:8: error: use of deleted function 'A::A(const A&)'
main.cpp:13:5: error: declared here
If I implement copy-constructor like this:
A(const A& a)
{
cout << "A(const A&)\n";
var = a.var;
}
Then code compiles fine and program gives me expected output:
A(int i)
b.a.var = 7
So it means that copy constructor is not used, but why I can't delete it?
Edit: Thanks for your answers. Copy or move constructor is required by standard if I'm using =. To fix this problem I need to implement move constructor or use direct initialization syntax A a{7}.
The initialiser for a gives you copy-initialization:
A a = 7;
For such a copy-initialization, where a user-defined conversion is required, the resulting initialisation is equivalent to:
A a(A(7));
That is, a temporary A is constructed and then passed to the copy constructor of your a object. This copying may be elided, but the copy constructor must be available nonetheless. In other words, the copying can only be elided if the copy would be possible in the first place. If you delete the copy constructor, the copying is impossible.
You'll have a better time with your deleted copy constructor if you do the following:
A a{7};
This does direct-initialization and no copy constructor is required.
Copy initialization is allowed to elide the copy but the copy constructor is mandated to be accessible by the standard.
Per Paragraph 12.2/14 of the C++11 Standard:
The initialization that occurs in the form
T x = a;
as well as in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and aggregate member initialization (8.5.1) is called copy-initialization. [ Note: Copy-initialization may invoke a move (12.8). —end note ]
The reason why your copy-initialization doesn't compile is that during copy-initialization a temporary object needs to be created (at least logically), and the object being initialized shall be constructed from it.
Now all the previous answers seem to focus just on copy-constructors, but the first problem here is the absence of a move-constructor. As long as you provide one, then it is true that the copy constructor is not necessary.
Alas, deleting the copy constructor prevents the generation of an implicit move constructor. Adding one explicitly would fix the problem:
struct A
{
int var;
A()
{
cout << "A()\n";
}
A(int i)
{
cout << "A(int i)\n";
var = i;
}
A(const A&) = delete;
// THIS MAKES IT WORK
A(A&& a)
{
cout << "A(A&&)\n`;
var = a.var;
}
};
Notice that when both the move-constructor and the copy-constructor are present, the move-constructor is preferred, because the temporary created for copy-initializing your object is an rvalue.
When a move-constructor is absent, the compiler can invoke the copy constructor to perform the initialization, because constant lvalue references can bind to rvalue references and copy is seen as an unoptimized move.
However, even though the compiler is allowed to elide the call to the move or the copy constructor, the semantics of the operation must still be checked. Per Paragraph 12.8/32 of the C++11 Standard:
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. [ 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 ] [...]
Therefore, an error is issued by the compiler if neither the move constructor nor the copy constructor are present.
If you want, however, you can direct-initialize your object rather than copy-initializing it. Just use the direct-initialization syntax instead:
struct B
{
A a{7};
};
This will make the move-constructor and copy-constructor unnecessary, because no temporary is created when direct-initializing an object.
This code doesn't compiles because of deleted copy-constructor which isn't necessary here
Sorry, but your copy constructor is necessary. Even though the copy can be optimised out, it must still be possible in the code. This mandated by the language.
So it means that copy constructor is not used, but whyI can't delete it?
In your case, copy constructor is used only for semantic-check as required by the Standard, it also needs to be accessible. Later on, the compiler optimizes the code, eliding the call to the copy-constructor, so it is not actually invoked.

copy elision: move constructor not called when using ternary expression in return statement?

Consider the following example:
#include <cstdio>
class object
{
public:
object()
{
printf("constructor\n");
}
object(const object &)
{
printf("copy constructor\n");
}
object(object &&)
{
printf("move constructor\n");
}
};
static object create_object()
{
object a;
object b;
volatile int i = 1;
// With #if 0, object's copy constructor is called; otherwise, its move constructor.
#if 0
if (i)
return b; // moves because of the copy elision rules
else
return a; // moves because of the copy elision rules
#else
// Seems equivalent to the above, but behaves differently.
return i ? b : a; // copies (with g++ 4.7)
#endif
}
int main()
{
auto data(create_object());
return 0;
}
And consider this bit from the C++11 Working Draft, n3337.pdf, 12.8 [class.copy], point 32:
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. [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 ]
Thus, if we use #if 1 in the example, the move constructor is first tried when returning the object, and then the copy constructor. Since we have a move constructor, it is used instead of the copy constructor.
In the last return statement in create_object() however, we've observed that the move constructor is not used. Is or is this not a violation of the language rules? Does the language require that the move constructor is used in the last return statement?
The specification for the conditional operator is so complicated it is scary. But I believe that your compiler is correct in its behavior. See 5.16 [expr.cond]/p4:
If the second and third operands are glvalues of the same value
category and have the same type, the result is of that type and value
category ...
Also see 12.8 [class.copy], p31, b1 which describes when copy elision is allowed:
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 ...
The expression is not the name of an automatic object, but is a conditional expression (and that conditional expression is an lvalue). So copy elision is not allowed here, and there is nothing else that says that the lvalue expression can pretend to be an rvalue for overload resolution.