Search code examples
c++operator-overloadingimplicit-conversionoverload-resolution

Overloaded user defined conversion operator based on value category of object selecting counter-intuitive overload


Can someone please explain which language rules are in play in the following example:

#include <iostream>
#include <type_traits>


template<typename T>
struct Holder{
    T val;

    constexpr operator T const&() const& noexcept{
        std::cerr << "lvalue\n";
        return val;
    }
    constexpr operator T ()  && noexcept(std::is_nothrow_move_constructible_v<T>) {
        std::cerr << "rvalue\n";
        return std::move(val);
    }
};

int main(){

    Holder h1{5};
    const int& rh1 = h1;
    const int& rh2 = Holder{7};
    std::cout << rh1 << ' ' << rh2 << '\n';
}

Output:

lvalue
lvalue

While I expected either ambiguity or rvalue for the second call.

If this example is run with address sanitizer it reveals the stack-use-after-scope error. I'd be really grateful if one could explain to me precisely that why isn't this ambigous or why isn't the rvalue ref overload selected?

My way of thinking is that, even though the target type is a perfect match, the const& member function uses an implicit conversion from rvalue to lvalue ref on the object itself,but the second overload is better in terms of member function ref qualifier, but uses an implicit rvalue to const lvalue ref conversion on the return type. I was under the strong impression that this initialization is driven by the initialization expression, but after analyzing the AST with clang I realized the source of the problem is that the target type is resolved first selecting the const& overload.

Note:I tried all possible combinations in terms of ref, cv qualification on both the function and the return type


Solution

  • The first relevant rule in the chain of initialization rules is [dcl.init.ref]/5.1.2.

    It states that an attempt is made to directly initialize the reference with the result of a call to a conversion function as determined by [over.match.ref].

    When initializing a lvalue reference to object type [over.match.ref] will consider only conversion functions to lvalue reference types (and only those for which it is also possible to bind the initialized reference directly to the result).

    This means, in this phase overload resolution is done without considering your second conversion function. Because you qualified the first conversion function with const&, it is viable for both the lvalue initializer in const int& rh1 = h1; as well as the rvalue initializer in const int& rh2 = Holder{7};.

    Only if there is no viable conversion in this first attempt at overload resolution, then you fall through in the initialization rules to [dcl.init.ref]/5.4.1 which attempts to initialize a temporary object of type int as if by copy-initialization from the initializer expression and would consider both conversion functions according to [over.match.conv].

    In other words, the initialization rules strongly prefer a conversion function to lvalue reference over others when initializing a lvalue reference, if that would allow binding to the result directly without creating a temporary object, with higher priority than other overload resolution disambiguations.