Search code examples
c++c++11visual-c++move-semanticsstandards-compliance

The local variable which is used in return statement doesn't convert to r-value implicitly to match the conversion operator


In the example snippet code below, the local variable which is used in return statement doesn't convert to r-value implicitly to match the conversion operator. However for move constructor it works.

I want to know whether it is a standard behavior or a bug. And if it is a standard behavior, what is the reason?

I tested it in Microsoft Visual Studio 2019 (Version 16.8.3) in 'permissive-' mode and it produced a compiler error. But in 'permissive' mode, it was OK.

#include <string>

class X
{
    std::string m_str;
public:
    X() = default;
    X(X&& that)
    {
        m_str = std::move(that.m_str);
    }
    operator std::string() &&
    {
        return std::move(m_str);
    }
};

X f()
{
    X x;
    return x;
}

std::string g()
{
    X x;
    return x; // Conformance mode: Yes (/permissive-) ==> error C2440: 'return': cannot convert from 'X' to 'std::basic_string<char,std::char_traits<char>,std::allocator<char>>'
    //return std::move(x); // OK
    // return X{}; // OK
}

int main()
{
    f();
    g();
    return 0;
}

Solution

  • The reason that f works under the C++11 standard (link is to a close-enough draft) is this clause

    [class.copy]/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. ...

    And the "criteri[on] for elision of a copy operation" that is relevant in this case is

    [class.copy]/31.1

    • 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

    This works for f, since x in return x is the "name of a non-volatile automatic object ... with the same cv-unqualified type as the function return type"; that type is X. This does not work for g, since the return type std::string is not the type X of the object named by x.

    I think it might be important to understand why this rule is here in the first place. This rule isn't really about implicitly moving function-local variables into function return values, even though that's what it literally says. It's about making NRVO possible. Consider what you would have to write for f without these rules:

    X f() {
        X x;
        return std::move(x);
    }
    

    But then NVRO cannot apply, since you aren't returning a variable; you're returning the result of a function call! So the clause [class.copy]/32 is about making your code

    X f() {
        X x;
        return x;
    }
    

    syntactically legal, while the semantics as described by the clause (using a move constructor) are to be ignored (assuming your implementation isn't too stupid) because we're actually just going to do NRVO, which doesn't call anything.

    You see that, really, [class.copy]/32 doesn't have to work for g. Its purpose in f is to make it possible to execute zero copy/move constructors. But g has to execute the conversion operator; there's no other sensible way for the language to pull out a std::string when you give it an X. So NVRO cannot apply in g, so there's no need to write return x;, so you can just write

    std::string g() {
        X x;
        return std::move(x);
    }
    

    and not be worried that will cause a missed optimization.

    We see that the C++11 rule [class.copy]/32 is designed so that it affects the minimal portion of the cases possible. It applies to those cases where'd we'd like NVRO but don't have a copy constructor, and makes NVRO possible by telling us to pretend we'll call the move constructor. But when actually writing code, that means it's a mind-twister of a rule to remember: "to minimize copies/moves, if the return type is the same as the type of the variable, return the_variable;, and otherwise return std::move(the_variable)." That's why the C++20 standard completely rephrases [class.copy]/32 into

    [class.copy.elision]/3

    An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:

    • If the expression in a return ([stmt.return]) or co_­return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or
    • ...

    overload resolution to select the constructor for the copy or the return_­value overload to call is first performed as if the expression or operand were an rvalue. ...

    This does not require that the return type be the same as the variable's type for an implicit move; it can be summed up as the conceptually simpler rule "returning a variable tries to move, and then tries to copy". That leads to the conceptually simpler principle "When returning a variable from a function, just return the_variable; and it will do the right thing". (Of course, none of GCC, Clang, or MSVC seem to have gotten the memo. That has to be some kind of record...)