Search code examples
c++referencelanguage-lawyerdirect-initialization

In which case(s) user-defined conversions are not considered during reference initialization?


Consider a case where we've reached into bullet [dcl.init.ref]/(5.4.1) during reference binding:

(5.4.1) If T1 or T2 is a class type and T1 is not reference-related to T2, 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.

How, during direct initialization of a reference, user-defined conversions are not considered?

If possible, support the answer with an example in which this rule is applied.


Solution

  • As far as I know, the bolded sentence is redundant. It may have been added out of an abundance of caution.

    The predecessor of the quoted paragraph, in C++11, actually required a temporary of type "cv1 T1" to be copy-initialized from the initializer expression, whereupon the reference to type "cv1 T1" (that we are trying to initialize) would be bound to that reference. Simple, right? There is no question of how such binding is to occur. The reference simply becomes an alias for that temporary object and we are done.

    When CWG1604 was resolved just prior to the publication of C++14, the wording was changed so that, instead of this paragraph requiring the copy-initialization of a temporary of type "cv1 T1", we instead do the following:

    • Determine the user-defined conversion function f that would be used for a hypothetical copy-initialization of a cv1 T1 object from the initializer expression;
    • Call the function f on the initializer expression. Denote the result of this by x;
    • Finally, direct-initialize our reference (call it r) from r.

    Because x might not be of type cv1 T1, a second run through the reference initialization algorithm is required in order to determine how to direct-initialize r from x. The old wording didn't require a second run. The drafters may have been concerned that there would be an edge case where the new wording would require consideration of user-defined conversions in the second run. But I think we can prove that there are no such cases.

    We can consider all possible ways in which the copy-initialization of a hypothetical cv1 T1 object could be done by consulting [dcl.init.general]/17, which governs copy-initializations:

    • p17.6.1 would not be applicable because if T1 and T2 are identical, then we would not be coming from [dcl.init.ref]/5.4.1 in the first place.
    • p17.6.2 would not be applicable because if T1 is a base class of T2, then we would not be coming from [dcl.init.ref]/5.4.1 in the first place.
    • If p17.6.3 applies, then f might be selected from the converting constructors of T2 (see [over.match.copy]/1.1). In this case the second run will be trivial: r is bound directly to x.
    • If p17.6.3 applies and a conversion function of T2 (possibly inherited) is selected then, as specified by [over.match.copy]/1.2, f must return a type cv3 T3 such that T3 is derived from T1. This implies that T1 is reference-related to T3, and on the second run, due to T1 being reference-related to T3, we never reach any of the clauses telling us to consider user-defined conversions. So even without the bolded sentence, user-defined conversions would be irrelevant.
    • Otherwise, p17.6.4 applies; T1 is a non-class type, and f is a conversion function of T2 (possibly inherited). As specified in [over.match.conv]/1.1, f must yield a type (i.e., x must be of a type) that is convertible to T1 by a standard conversion sequence. A standard conversion sequence can never convert a class type to a non-class type, so in this case x is of a non-class type. Since T1 is of non-class type and x is also of non-class type, during the second run, user-defined conversions will be irrelevant even without the bolded sentence.
    • The remaining cases in p17.6 deal with cases where T1 and T2 are both of non-class type, so we would not reach them if we are coming from [dcl.init.ref]/5.4.1.

    Note that all references above are to the C++20 standard.

    I suspect that the resolution to this issue might have been drafted in a hurry since it was not fixed until after the committee draft had been sent out and it appears that the Canadian NB asked the committee to fix it. The drafters might have decided to just insert the clause about not using user-defined conversions rather than doing an in-depth study to determine whether it is needed. In addition, it seems "cleaner" to avoid even conceptual recursion (i.e. instead of re-running the reference initialization algorithm with different inputs on which the logic of the algorithm guarantees that a further recursive call won't occur, we run a more limited algorithm).