2

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?

12
  • Change X&& r = z; to const X&& r = z;. Commented Aug 13, 2024 at 9:59
  • 1
    "But in 5.4.1, the temporary object is created as T1." That's not true. 5.4.1 says the result of the conversion function is used to initialize the reference. It doesn't say that a temporary is created. Commented Aug 13, 2024 at 10:03
  • 1
    @user12002570 This will fall into the case in 5.3.2. which is not what I want to test. I want to test 5.4.1 using conversion function. As stated in 5.4.1, z should be converted to a temporary of type "cv1 T1" (which is type X in my case), the compiler complained about "const" meaning that it fails to handle the case in 5.4.1. Note that X r = z; it can compile which means that I can use z to copy-initialize a type X (which should be the same type as the temporary) Commented Aug 13, 2024 at 10:08
  • 1
    @user12002570 No. In section 5, the standard stated that "A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows", so it is clear that cv T1 does not include "const" Commented Aug 13, 2024 at 10:22
  • @cppleaner In 5.4.1, "user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion", which does create a temporary. Commented Aug 13, 2024 at 10:32

3 Answers 3

3

The program(X &&r = z;) is ill-formed as explained below.

First, X &&r = z; is reference initialization so we go to dcl.init.ref:

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

  • [...]
  • [...]
  • [...]
  • Otherwise, T1 shall not be reference-related to T2.
    • 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]); the program is ill-formed if the corresponding non-reference copy-initialization would be ill-formed. 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.

This means that the result of the conversion function which will be of type const Y will be used to direct initialize r.

So the initialization process will be repeated again. This time we first go to direct initialization:

If the destination type is a reference type, see [dcl.init.ref].

So now come to dcl.init.ref and see that none of the bullet points is applicable for initialization of the reference r from const Y.

In particular, 5.1 is not applicable because the reference is not lvalue reference.

Similarly 5.2 is not applicable.

5.3 is also not applicable.

5.4 is not applicable because const Y is reference related to X as X is a base.

So none of the bullet points are applicable for the direct initialization of r by const Y and so X&& r = z; is ill-formed.

Sign up to request clarification or add additional context in comments.

3 Comments

But how can you explain the sentence user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion. Basically, your answer means that a temporary of type X is not created by copy-initialization. Instead, the result of the conversion function, which is const Y prvalue is used to direct initialize the reference X&& r. If this is true, that means I misunderstood the wording. I think it is quite confusing that the standard mentioned copy-initialization of an object of cv1 T1.
@CppCoder That means that reference initialization has some more restrictiion than copy initialization. That is, if the corrresponding copy initialization is ill-formed then the reference initialization will also be ill-formed but if the correspnding copy initialization is well-formed that does not guarantee that the reference initialization will also be well-formed. In other words, reference initialization has an additional restriction.
@user12002570 So it is just used to select the conversion function, the copy-initialization is not actually performed?
2

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:

  • The converting constructors of X, that is, all its non-explicit constructors. There are three: the default, copy, and move constructor. However, according to [over.best.ics.general]/4.4, we cannot use any user-defined conversions to convert the initializer to the parameter type of the converting constructor. (This rule prevents having multiple user-defined conversions in one implicit conversion sequence, and in some cases also prevents infinite recursion in the overload resolution procedure.) And there is of course no standard conversion from Z to const X& or X&&, so none of the converting constructors are viable.
  • The conversion functions of Z that convert to X, any class derived from X, cv-qualified versions of the aforementioned types, or references thereto ([over.match.copy]/1.2; [over.match.funcs.general]/7). There is one such conversion function, Z::operator const Y, so it is chosen.

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.

2 Comments

I changed to accept this answer because it explained that the different purpose of 5.3.2 and 5.4.1 and explained that 5.4.1 is not redundant. That's my original motivation to write the test code - to test the case in 5.4.1. Both this answer and Alan's answer also explained that the copy-initialization is not actually preformed and it is just a process to select the best user-defined conversion. This answer my question of "why operator const Y is not allowed?". Thank you all for answering my question!
Also, cpplearner's answer actually explained the same thing, which is also correct. Thank you for the answer!
0

I think this is more of an English question.

When the standard says that "user-defined conversions are considered using the rules for copy-initialization", it doesn't mean that copy-initialization is performed. Instead, it means that user-defined conversions (which is defined as "constructors and conversion functions" in [class.conv]) are examined using the same rules as those for copy-initialization, and the best viable one is called.

Then, 5.4.1 goes on to specify that the result of the call is used to initialize the reference. This means, in your example, the result of operator const Y() is directly uses to initialize r, without an intermediate temporary of type X. (And that initialization is ill-formed.)

In any case, if you find the wording to be unclear or defective, you can create an issue at https://github.com/cplusplus/CWG

4 Comments

Assume that the standard intends to directly use the result of operator const Y, this case is already covered in 5.3.2, so is it redundant or what is the purpose of including conversion function in 5.4.1?
There are a lot of DR around this area, maybe it's now safe to strike conversion functions in 5.4.1.
Though currently struct A { operator int(); }; long&& r = A{}; is handled by 5.4.1.
Thank you for providing the last example. int is not reference-compatible withlong and it indeed used 5.4.1 to make the binding possible. But it seems that it also showed that a temporary of long is created in 5.4.1. Otherwise, the long reference cannot bind to an int. So my question of "why operator const Y is not allowed?" is still not answered.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.