Class with all automatically-generated constructors/operators deleted can still be returned from a function?
Asked Answered
A

2

19

Recently, I came across this answer which describes how to initialize a std::array of non-default-constructible elements. I was not so surprised because that answer clearly doesn't do any default-constructing.

Instead, it is constructing a temporary std::array using aggregate initialization, then moving (if the move constructor is available) or copying into the named variable when the function returns. So we only need either the move constructor or copy constructor to be available.

Or so I thought...

Then came this piece of code which confounded me:

struct foo {
    int x;
    foo(int x) : x(x) {}
    foo() = delete;
    foo(const foo&) = delete;
    foo& operator=(const foo&) = delete;
    foo(foo&&) = delete;
    foo& operator=(foo&&) = delete;
};

foo make_foo(int x) {
    return foo(x);
}

int main() {
    foo f = make_foo(1);
    foo g(make_foo(2));
}

All the five special member constructors/operators are explicitly deleted, so now I shouldn't be able to construct my object from a return value, correct?

Wrong.

To my surprise, this compiles in gcc (with C++17)!

Why does this compile? Clearly to return a foo from the function make_foo(), we have to construct a foo. Which means that in the main() function we are assigning or constructing a foo from the returned foo. How is that possible?!

Almita answered 20/7, 2018 at 13:24 Comment(1)
Try compiling with the -std=c++11 or -std=c++14 flags to observe different results.Connecticut
D
29

Welcome to the wonderful world of guaranteed copy elision (new to C++17. See also this question).

foo make_foo(int x) {
    return foo(x);
}

int main() {
    foo f = make_foo(1);
    foo g(make_foo(2));
}

In all of these cases, you're initializing a foo from a prvalue of type foo, so we just ignore all the intermediate objects and directly initialize the outermost object from the actual initializer. This is exactly equivalent to:

foo f(1);
foo g(2);

We don't even consider move constructors here - so the fact that they're deleted doesn't matter. The specific rule is [dcl.init]/17.6.1 - it's only after this point that we consider the constructors and perform overload resolution.

Defaulter answered 20/7, 2018 at 13:26 Comment(0)
L
0

Notice that pre-C++17 (before guaranteed copy elision), you might already return that object with braced-init-lists:

foo make_foo(int x) {
    return {x}; // Require non explicit foo(int).
                // Doesn't copy/move.
}

But usage would be different:

foo&& f = make_foo(1);
foo&& g(make_foo(2));
Lowry answered 20/7, 2018 at 14:23 Comment(1)
Is the lifetime of foo&& f extended in the same way as const foo& f?Almita

© 2022 - 2024 — McMap. All rights reserved.