Christopher Leong
Christopher Leong

Reputation: 402

LValue ref qualified member function being called on an RValue object

I'm trying to figure out why the following snippet calls the LValue cast operator overload:

#include <iostream>

class Foo
{
public:
    Foo(int i = 0) : i(i) {}

    operator const int& () const &
    {
        std::cout << "lvalue\n";
        return i;
    }
    
    operator int () const &&
    {
        std::cout << "rvalue\n";
        return i;
    }
    
    int i = 0;
};

Foo Fool()
{
    return Foo(5);
}

int main()
{
    const int& i = Fool();
    const int j = Fool();
    
    return 0;
}

The current outputs are:

lvalue

rvalue

But from my understanding Fool() returns an rvalue and since const& can bind to rvalues there is no need to construct an lvalue Foo.

Can anyone explain why lvalue is being constructed? I believe this is a dangling lvalue.

Upvotes: 5

Views: 1282

Answers (1)

Okay, so the thing to note here is that overload resolution only ever considers one conversion function for i. They don't both participate, and so the reference qualifier cannot be used to differentiate them. For the case of binding a reference

[over.match.ref]

Under the conditions specified in [dcl.init.ref], a reference can be bound directly to the result of applying a conversion function to an initializer expression. Overload resolution is used to select the conversion function to be invoked. Assuming that “reference to cv1 T” is the type of the reference being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “cv2 T2” or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where “cv1 T” is reference-compatible with “cv2 T2”, are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type “lvalue reference to cv2 T2” (when initializing an lvalue reference or an rvalue reference to function) or “rvalue reference to cv2 T2” (when initializing an rvalue reference or an lvalue reference to function), where T2 is the same type as T or can be converted to type T with a qualification conversion, are also candidate functions.

According to the text in bold, when initializing i, our only candidate is operator int const&. So overload resolution can either pass here, or fail entirely. But it cannot select operator int, since that one is not even under consideration. It succeeds because a const qualified lvalue reference can bind to the object argument.

On the other hand, for initializing a value

[over.match.conv]

Under the conditions specified in [dcl.init], as part of an initialization of an object of non-class type, a conversion function can be invoked to convert an initializer expression of class type to the type of the object being initialized. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T” is the type of the object being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T via a standard conversion sequence are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T with a qualification conversion are also candidate functions. Conversion functions that return a cv-qualified type are considered to yield the cv-unqualified version of that type for this process of selecting candidate functions. A call to a conversion function returning “reference to X” is a glvalue of type X, and such a conversion function is therefore considered to yield X for this process of selecting candidate functions.

So when initializing j both conversion functions participate as overloads, and here the reference qualifier makes a difference.

You do get a dangling reference here, and it seems to be due to a dark corner in the language. The bullet in the first quoted paragraph could probably be refined to consider the binding of const lvlaue references better. Since those may bind to temporaries as well, your second conversion operator could ideally be a candidate under better rules.

Upvotes: 2

Related Questions