Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unintuitive RVO of function returning non-copyable const value?

Consider the following example code in C++ >=17:

struct A{
    A() = default;
    A(const A&) = delete;
};

const A f(){ return A{}; }

int main(){
    const A& a = f(); // OK
    // A& b = f();    // Error: cannot convert 'const A' to 'A&'
    const A c = f();  // OK: Copy elision
    A d = f();        // OK!?
}

The class A is non-copyable, but because of the mandatory copy-elision, we can put the result of f() into variables. According to this page in cppreference.com, the above behavior is perfectly legitimate, since it is specified that the const quantifier on the returning value is ignored when copy-elision happens.

However, this behavior seems very counterintuitive to me. Since A is non-copyable, I feel like there should be no way to turn const A into A (except perhaps when you have A::A(const A&&) constructor). Is this a well-thought decision, or is this considered a defect in language specification?

(I have encountered this problem when trying to implement my own type-erasure class. The whole purpose of me specifying const on the return value of function f() was to prevent the user from getting a non-const lvalue reference of the object, but this specification seems to open a hole.)

Edit: This example might show the counter-intuition more clearly: Let's consider a class A that is movable but not copyable.

struct A{
    A() = default;
    A(const A&) = delete;
    A(A&&) = default;
};

int main(){
    // C++14: move / C++17: copy elision
    A a = A{}; 

    // C++14: error (deleted copy constructor) / C++17: copy elision(!!)
    A b = static_cast<const A>(A{}); 
}

This does not compile in C++ <=14, but compiles in C++ >=17. In a common case where a class is movable but not copyable, const meant there were no means to get a non-const object out of it before C++14, but it doesn't anymore (as long as const is added to a prvalue).

like image 865
eivour Avatar asked Apr 24 '21 17:04

eivour


1 Answers

It would be a breaking change to reject such an initialization with a movable object, since prior versions of the language would produce a move there. Making it depend on the cv-qualification of the variable would have been very subtle.

For a copyable object, the new behavior is actually a subset of the old: the copy from the const A return value to the A variable could have been elided, in which case they were just as much the same object as in C++17.

Meanwhile, const return values have been somewhat frowned upon since C++11, where f(return_const()) lost the ability to move into a (by-value) parameter.

The C++17 treatment of prvalues (“mandatory copy elision” is a name that makes sense only historically) enables yet other cases, like the return of non-movable objects in this example: the function is thought to specify how to initialize the object it “returns”, rather than actually returning a finished object. In choosing this model, it was generally considered more important to support more kinds of efficient code than to support existing idioms for preventing misuse.

like image 98
Davis Herring Avatar answered Sep 23 '22 07:09

Davis Herring