How to Invoke a User-Defined Conversion Function via List-Initialization

Why does user-defined conversion applied during the initialization?

The wording was defective and changed with C++14. Now [over.best.ics]/4 reads

However, if the target is

  • the first parameter of a constructor or
  • […]

and the constructor or user-defined conversion function is a candidate
by

  • 13.3.1.3, when the argument is the temporary in the second step of a class copy-initialization,
  • 13.3.1.4, 13.3.1.5, or 13.3.1.6 (in all cases), or
  • the second phase of 13.3.1.7 when the initializer list has exactly one element, and the target is the first parameter of a
    constructor of class X, and the conversion is to X or reference to
    (possibly cv-qualified) X,

user-defined conversion sequences are not considered. [ Note: These
rules prevent more than one user-defined conversion from being applied
during overload resolution, thereby avoiding infinite recursion.
end note
]

The conversion of B() to int is not covered by this - the bold phrase only appertains to the binding of a reference to a temporary during copy-initialization.
However, Clang rejects this sample code according to the above:

class A;

struct B
{
operator A();
};

struct A
{
A(A const&){}
};

A a{B()};

Initialization by conversion function for direct reference binding

Why do we even need a conversion function to convert from cv2 T2 to cv1 T even though “cv1 T” is reference-compatible with “cv2 T2”?

Because only after the conversion is done(using the conversion function), the result is of type cv2 T2. In other words, the result of using/calling the conversion function is cv2 T2 and not whatever expression you have before using the conversion function. This is the whole point of using a conversion function so that we can something that is reference compatible with cv1 T.

Lets look at some examples:

Example 1

struct C {
operator int&();
};

const int& ref= C();

In the above example, the type cv1 T = const int is not reference-compatible with the class-type C. But due to the presence of the conversion function C::operator int&(), a given C can be converted to an lvalue of type cv2 T2 = int. And we know that const int is reference compatible with int. This means that the reference ref can be bound to the result of the conversion function according to your quoted clause.

Example 2

This example 2 does not uses a conversion function but i've added this just to clarify the explanation given at the beginning of my answer.

double val = 4.55;   
const int &ref = val;

In the above example, the type cv1 T = const int is not reference-compatible with the type double. And at the end, the reference ref is bound to a materialized temporary(prvalue) of type cv2 T2 = int. This is possible because const int is reference compatible with the type of the materialized temporary int.

The important thing to note here is that a temporary is materialized because originally cv1 T = const int was not reference compatible with double. But after the materialization, cv1 T = const int is reference compatible with the result cv2 T2 = int.

What's the rank of implicitly conversion for copy-list-initialization

Answers are here,Because the argument is initializer list,so [over.ics.list] rules are performed when overload resolution.

[over.ics.list]/6


  1. Otherwise, if the parameter is a non-aggregate class X and overload resolution per [over.match.list] chooses a single best constructor C of X to perform the initialization of an object of type X from the argument initializer list:

    • If C is not an initializer-list constructor and the initializer list has a single element of type cv U, where U is X or a class derived from X, the implicit conversion sequence has Exact Match rank if U is X, or Conversion rank if U is derived from X.
    • Otherwise, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion.

Hence,for these three candiate constructors,the parameter of them are all non-aggerate class type,so the implicit conversion sequence is used to convert the single element of the initializer list to corresponding parameter type of the constructor of parameter of constructor B and therefore these conversion sequences are all user-defined conversion sequence and the second standard conversion sequence are all identity conversions.So the rank of them are indistinguishable.

When are user-defined conversion sequences not considered for selecting viable constructors in C++?

You can go read [dcl.init]/17 for the actual standardese. The "second step" here is referring to copy-initializing a variable of class type A from something b of an unrelated type. In such a case, copy-initialization happens in two steps:

  • Step 1: you implicitly convert b to A. If you call a converting constructor for this, it creates a temporary A.
  • Step 2: you then initialize the A variable from the result of the conversion. (In sane classes, this is typically elided.)

What that quote is saying is that you don't do user-defined conversions in this second step.

For example, with your A, A a = 0;. In the first step, you make an A temporary from 0. In the second step, you try to initialize a with that temporary - without using user-defined conversions. That fails, because neither A constructor is viable.

Conversion operator in direct-initialization

You are correct that only the constructors of A are considered when doing A a(b);. [over.match.ctor]/1 states

When objects of class type are direct-initialized, copy-initialized from an expression of the same or a derived class type ([dcl.init]), or default-initialized, overload resolution selects the constructor. For direct-initialization or default-initialization that is not in the context of copy-initialization, the candidate functions are all the constructors of the class of the object being initialized. For copy-initialization (including default initialization in the context of copy-initialization), the candidate functions are all the converting constructors ([class.conv.ctor]) of that class. The argument list is the expression-list or assignment-expression of the initializer.

emphasis mine

This means that A(), A(const A&) and A(A&&) are the candidate list. Then we have [over.match.viable]/4

[...]Third, for F to be a viable function, there shall exist for each argument an implicit conversion sequence that converts that argument to the corresponding parameter of F.[..]

which allows an implicit conversion of b to an A so that A(A&&) can be called.

Return value is copied when using initializer list with a user-defined conversion operator that returns reference

You are actually doing reference initialization here:

Nothing & by_initialization_2{wrapper};

The rules say that since the initializer is not the same type as the reference being bound, user-defined conversion operators are considered, which is fine, since you have the appropriate conversion operator.

However, if the l-value returned by the conversion function is passed through a brace-init list, then a temporary is materialized. Since you can't bind a non-const reference to a temporary, the initialization fails.



Related Topics



Leave a reply



Submit