Reputation: 389
I want to create an object in a function and use it outside.
I write the following code under the c++17 standard, it seems ok.
#include <iostream>
struct Vector2 {
Vector2() = default;
Vector2(int x, int y) : x(x), y(y) {}
Vector2(const Vector2 &) = delete;
Vector2 &operator=(const Vector2 &) = delete;
int x = 0;
int y = 0;
};
Vector2 newVec(int x, int y) {
return Vector2(x, y);
}
int main() {
auto v = newVec(1, 2);
std::cout << v.x * v.y << std::endl;
return 0;
}
But when I switched to c++11 standard, I couldn't compile it.
note: 'Vector2' has been explicitly marked deleted here Vector2(const Vector2 &) = delete;
I think I constructed a temporary Vector2 object in the newVec function. When return, Vector2 call copy constructor with the temporary variable as the argument. But I have marked copy constructor 'delete', so it can't be compiled.
My question is what is the equivalent code under the C++11 standard? What did c++17 do to avoid this copy constructor?
Upvotes: 2
Views: 252
Reputation: 304142
What did c++17 do to avoid this copy constructor?
This:
auto v = newVec(1, 2);
was one of the motivating cases for a language feature called "guaranteed copy elision." The example from that paper:
auto x = make(); // error, can't perform the move you didn't want, // even though compiler would not actually call it
Before C++17, that expression always copy-initialization. We deduce the type with auto
, that gives us Vector2
, and then we're trying to construct a Vector2
from an rvalue of type Vector2
. That's the usual process - enumerating constructors, etc. The best match is your copy constructor, which is deleted, hence the whole thing is ill-formed.
Sad face.
In C++17, this is totally different. Initializing from a prvalue of the same type doesn't try to find a constructor at all. There is no copy or move. It just gives you a Vector2
that is the value of newVec(1, 2)
, directly. That's the change here - it's not that in C++17 that copy-initialization works, it's more that it's not even the same kind of initialization anymore.
My question is what is the equivalent code under the C++11 standard?
There is no way at all to construct a non-movable object like this. From the same paper:
struct NonMoveable { /* ... */ }; NonMoveable make() { /* how to make this work without a copy? */ }
If you want a function like make()
(newVec()
in your example) to work, the type has to be, at least, movable. That means either:
unique_ptr<Vector2>
, which is movableOr you pass into make()
your object and have the function populate it internally:
void make(NonMoveable&);
This ends up being less compose-able, but it works. As written, it requires your type to be default constructible but there are ways around that as well.
Upvotes: 6
Reputation: 1011
You need to either remove the explicit deletion of the copy constructor so that the move constructor gets generated(but so will be the copy constructor) or declare a move constructor like so:
Vector2(Vector2 &&) = default;
According to the standard,
If the definition of a class X does not explicitly declare a move constructor, one will be implicitly declared as defaulted if and only if
— X does not have a user-declared copy constructor,
— X does not have a user-declared copy assignment operator,
— X does not have a user-declared move assignment operator, and
— X does not have a user-declared destructor.
User-declared means either either user-provided (defined by the user), explicitly defaulted (= default) or explicitly deleted (= delete)
On the other hand in C++17, call to a copy/move constructor is omitted due to the reason described here:
Under the following circumstances, the compilers are permitted, but not required to omit the copy and move (since C++11) construction of class objects even if the copy/move (since C++11) constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to.
...
(Since C++17) In the initialization of an object, when the source object is a nameless temporary and is of the same class type (ignoring cv-qualification) as the target object. When the nameless temporary is the operand of a return statement, this variant of copy elision is known as RVO, "return value optimization". (until C++17)
Return value optimization is mandatory and no longer considered as copy elision; see above.
Upvotes: 2
Reputation: 13627
You are observing guaranteed copy elision in C++17.
On the other hand, C++11 requires that a copy or move constructor is present and accessible even if the compiler ultimately decides to elide its invocation.
Upvotes: 3
Reputation: 249642
You can fix it with two small changes:
return {x, y};
from newVec()
. This will construct the object just once and not require a copy constructor.const auto& v
instead of auto v
. This will have the same lifetime (but you can't modify it), and doesn't require a copy constructor.Upvotes: 1