c++referencelanguage-lawyerimplicit-conversionrvalue-reference

C++ reference initialization with conversion


I am trying understand the reference initialization in C++, especially for initializing lvalue reference to "const" and rvalue reference.

As I read the standard draft in here: https://eel.is/c++draft/dcl.init.ref#5.4.1

If T1 or T2 is a class type, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion ([dcl.init], [over.match.copy], [over.match.conv])

I found that when T2 requires a conversion to T1, apart from the conversion constructors of T1, it also allows conversion functions in T2.

Originally, I thought that this is redundant because the cases of conversion functions have already been covered in: https://eel.is/c++draft/dcl.init.ref#5.3.2

has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an rvalue of type “cv3 T3” or an lvalue of function type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3” (see [over.match.ref])

So, my first question is it redundant to include conversion functions in 5.4.1?

Later I found that there is a slightly difference in 5.3.2 and 5.4.1.

For example, in 5.3.2, T2 can be converted to a derived class of T1. But in 5.4.1, the temporary object is created as T1.

So I wrote the following simple code to test it:

#include<iostream>
class X
{
    public:
        virtual int get()
        {
            return 1;
        }
};
class Y : public X
{
    public:
        int get()
        {
            return 2;
        }
};
class Z
{
    public:
        operator const Y () const
        {
            return Y();
        }
};

int main()
{
    Z z;
    X&& r = z;
    std::cout << r.get() << std::endl;
    return 0;
}

Output in one of the compiler:

main.cpp: In function 'int main()':
main.cpp:46:12: error: binding reference of type 'X&&' to 'const X' discards qualifiers
   46 |   X&&  r = z;
      |            ^
main.cpp:40:7: note:   after user-defined conversion: 'Z::operator const Y() const'
   40 |       operator const Y () const {return Y();}

I expected that z is converted to a temporary object of type X as stated in 5.4.1. But it results in a compiler error saying that can't convert z to X. If I changed "operator const Y" to "operator Y", it can compile and print 2. But this actually falls into the case of 5.3.2, not 5.4.1, which is not what I wanted to test. Although the compiler says that it can't convert z to type X, if I changed "X&& r= z;" to "X r = z;", the compiler can compile and print 1 which means what it claims (can't convert z to type X) is wrong.

I tested it in both cpp.sh and coliru.stacked-crooked.com.

So it seems that the compiler fails to implement the case in 5.4.1 of using conversion functions, or did I misunderstand the standard draft?

Edit:

To make my question clear, I am intentionally using "operator const Y" instead of "operator Y" so that "const Y" is not reference-compatible to "cv1 T1" and it will not fall into the case 5.3.2. I expected 5.4.1 will be used and a temporary of type X (not const X) is copy-initialized from z. If 5.4.1. also requires "const X", it is redundant as 5.3.2. It will be meaningless to include the conversion functions in 5.4.1. (5.4.1. should only include conversion constructor). So I thought either the compiler is wrong (it fails to handle 5.4.1) or the standard is redundant (if the standard intends to require "const X" instead of "X"). Or anyone can provide a situation where 5.4.1 will use conversion functions?


Solution

  • Conversion functions are considered at three possible stages in [dcl.init.ref]/5:

    1. 5.1.2 applies when the reference is an lvalue reference and the initializer has a class type that has a conversion function to a compatible lvalue reference type.
    2. 5.3.2 applies when the reference is an rvalue reference or a const lvalue reference, and the initializer has a class type that has a conversion function to a compatible rvalue reference type or non-reference type. (Note that calling a conversion function that returns a non-reference type results in a prvalue, which is a kind of rvalue.) Note that in the case of a const lvalue reference, 5.1.2 (when applicable) takes priority over 5.3.2.
    3. 5.4.1 applies when the reference is an rvalue reference or const lvalue reference, and the initializer can be implicitly converted to the referenced type; that implicit conversion might use a conversion function. Note that 5.1.2 and 5.3.2 take precedence over 5.4.1.

    For example, if you try to initialize a const double&:

    1. In 5.1.2, the compiler will look for an operator double& or an operator const double&. If multiple candidates are found, the best one is chosen by overload resolution. If none are found, we have to go to step 2.
    2. In 5.3.2, the compiler will look for operator double, operator double&&, operator const double, or operator const double&&. If none are found, we have to go to step 3.
    3. In 5.4.1, the compiler will look for conversion functions that convert to any type that can be converted to const double by a standard conversion sequence, i.e., volatile double, const volatile double, all other qualified and unqualified arithmetic types, and qualified and unqualified unscoped enumerations, as well as lvalue and rvalue references to the aforementioned types. (Note that an implicit conversion sequence can only contain up to one user-defined conversion, and a conversion function counts as one, so the remaining conversions in the sequence must be standard conversions.)

    So 5.4.1 is not redundant.

    As for what actually happens in the X&& r = z; example, yes, this goes to 5.4.1 which instructs us to consider user-defined conversions as if we were copy-initializing an object of type X. Because X is of class type, this falls through to [dcl.init.general]/16.6.3, which in turn points to [over.match.copy]. There are two kinds of candidates:

    Note that [dcl.init.ref] doesn't actually tell us to perform the implicit conversion from the initializer to X; it just tells us to consider user-defined conversions and apply overload resolution to select one as if we were copy-initializing an X. Having chosen operator const Y, we then apply the last part of the bullet:

    The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference. For this direct-initialization, user-defined conversions are not considered.

    Alan's answer has already explained why this step fails, making the original reference initialization ill-formed.