0x11901
0x11901

Reputation: 389

How to avoid triggering this kind of copy constructor in c++11?

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

Answers (4)

Barry
Barry

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:

  • adding a move constructor
  • undeleting your copy constructor
  • if none of the above is possible, put it on the heap - wrap it in something like unique_ptr<Vector2>, which is movable

Or 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

Nellie Danielyan
Nellie Danielyan

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

j6t
j6t

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

John Zwinck
John Zwinck

Reputation: 249642

You can fix it with two small changes:

  1. return {x, y}; from newVec(). This will construct the object just once and not require a copy constructor.
  2. 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

Related Questions