cplusplus / CWG

Core Working Group
23 stars 7 forks source link

CWG2856 [class.conv.ctor] Copy list initialization with explicit and converting constructors #486

Closed ranaanoop closed 2 months ago

ranaanoop commented 6 months ago

Full name of submitter: Anoop Rana

Reference (section label): [class.conv.ctor]

Link to reflector thread (if any): Is default ctor a converting ctor

Issue description: Converting ctor in C++03 had to have at least one parameter. While C++11 changed this and stated that a ctor with more than one parameter can also be a converting ctor. In particular, according to C++11 as well as C++17:

A constructor declared without the function-specifier explicit specifies a conversion from the types of its parameters to the type of its class. Such a constructor is called a converting constructor.

Note that the above quoted reference does not say if a ctor with no parameter is a converting ctor. Then in C++17, the phrase if any was added:

A constructor declared without the function-specifier explicit specifies a conversion from the types of its parameters (if any) to the type of its class. Such a constructor is called a converting constructor.

Note the highlighted if any that was added in C++17. This seems to suggest that a default ctor such as C::C(){} is also a converting ctor.

Was this change intentional and needed for some particular code to work? I mean a ctor C::C(){} doesn't convert any parameter to the class type C so it doesn't make much sense to call it a converting constructor. So is a default constructor supposed to be a converting constructor.

This matters because the behavior of the following program depends on whether or not C::C() is also a converting ctor. We see implementation divergence for the following code.

struct A
{
  explicit A(int = 10);
  A()= default;
};

A a = {}; //msvc ok but gcc and clang fails here

The current wording seems to make this well-formed as described below. Not sure if this was the intended behavior.

First note that A a = {}; is copy-initialization.

15) The initialization that occurs in the = form of a brace-or-equal-initializer or condition ([stmt.select]), as well as in argument passing, function return, throwing an exception ([except.throw]), handling an exception ([except.handle]), and aggregate member initialization ([dcl.init.aggr]), is called copy-initialization.

Next we move to the semantics of the initializer.

17) The semantics of initializers are as follows. The destination type is the type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.

  • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized ([dcl.init.list]).

The above means that, the object is to be list-initialized.

From list initialization:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the initializer-list or designated-initializer-clauses of the designated-initializer-list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.

The above means that A a = {}; is copy-list initiaization. Next we see the effect of list initialization:

3) List-initialization of an object or reference of type T is defined as follows:

  • Otherwise, if the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

The above means that the object will be value initialized, so we move on to value-initialization:

8) To value-initialize an object of type T means:

  • if T has either no default constructor ([class.default.ctor]) or a default constructor that is user-provided or deleted, then the object is default-initialized;

This means that the object will be default initialized:

7) To default-initialize an object of type T means:

  • If T is a (possibly cv-qualified) class type ([class]), constructors are considered. The applicable constructors are enumerated ([over.match.ctor]), and the best one for the initializer () is chosen through overload resolution ([over.match]). The constructor thus selected is called, with an empty argument list, to initialize the object.

So we move on to over.match.ctor to get a list of candidate ctors. Also do note the initializer() part in the above quoted reference as it will be used at the end as an argument.

1) 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, the candidate functions are all the converting constructors of that class. The argument list is the expression-list or assignment-expression of the initializer.

This means that only the non-explict ctor A::A() is the candidate because it is a converting ctor while the other explicit A::A(int) is not.

Finally, since we only have one viable option, it is the one that is selected for the initializer ().

jensmaurer commented 6 months ago

The "if any" is intentional; see https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0398r0.html

and in particular the example that was added:

Z c = {};                 // error: copy-list-initialization

I think your interpretation is sound; please file bug reports against the deviating implementations.

I'm not seeing a wording defect here; the standard seems rather clear in this area.

t3nsor commented 6 months ago

I think there might be a real issue here: as Anoop points out, the normative wording appears to require the copy-non-list-initialization rule to be used for copy-initialization from {} (i.e. explicit constructors are not candidates for overload resolution) but the example, in saying "error: copy-list-initialization", appears to be alluding to [over.match.list], where the selection of an explicit constructor causes the program to be ill-formed after overload resolution succeeds.

I think the example reflects the intent, and the wording is wrong; a copy-initialization from {} should use the same rule as a copy-initialization from a nonempty braced-init-list, i.e., explicit constructors should be candidates, with the program being ill-formed if one is selected.

t3nsor commented 6 months ago

(Nevertheless, the wording is clear that default constructors are converting constructors if and only if they are non-explicit, just like constructors with any other number of arguments. So there is no core issue with the definition of "converting constructor". But, separately, I do think it is a confusing term and advocate abolishing it: https://github.com/cplusplus/draft/issues/6744)

jensmaurer commented 5 months ago

Aha, so the actual complaint is that these two copy-list-initialization contexts get different treatment per the normative wording:

bool f(C);
bool b = f({});   // [over.match.list], ill-formed if explicit constructor is chosen
C c = {};      // value-initialization -> default-initialization; ignore explicit constructors when forming candidates

If I remember correctly, we wanted to consider explicit constructors in f({}) because it's a bad trap to prefer another f overload just because there's an explicit (vs. implicit) constructor somewhere in the mix. Note that {} has no type, which make us consider more things in overload resolution.

Also, the paper that added the example did add "if any" (expressly including zero-parameter constructors) to the rule that is taken only for the C c = {} case, not the f({}), so I doubt we were confused back at the time which rule applied here.

Maybe the comment in the example is just a little off?

ranaanoop commented 5 months ago

Also, the below given program has different behavior in C++11 and C++20. In particular, it seems to be well-formed in C++11 while ill-formed in C++20(the c++20 case is explained here). Do note that even though the below program is well-formed in C++11, all compilers still reject it in C++11.

struct A {
  explicit A(int i = 42) {}
};
struct B {
  A a;
};

int main() {
  B b = {};
  return 0;
}

Below is the explanation why it is well-formed in C++11.

Here, B b = {}; is list-initialization:

List-initialization is initialization of an object or reference from a braced-init-list. Such an initializer is called an initializer list, and the comma-separated initializer-clauses of the list are called the elements of the initializer list. An initializer list may be empty. List-initialization can occur in direct-initialization or copy-initialization contexts; list-initialization in a direct-initialization context is called direct-list-initialization and list-initialization in a copy-initialization context is called copy-list-initialization.

Now we move onto dcl.init.list#3:

List-initialization of an object or reference of type T is defined as follows:

  • If the initializer list has no elements and T is a class type with a default constructor, the object is value-initialized.

The important thing to note here is that the above quoted reference does not say "user provided/declared default ctor" but only says "default ctor". And since B has a default ctor(which is implicitly generated by compiler), the object b is value initialized.

To value-initialize an object of type T means:

  • if T is a (possibly cv-qualified) non-union class type without a user-provided constructor, then the object is zero-initialized and, if T's implicitly-declared default constructor is non-trivial, that constructor is called.

Again not the emphasis on the "user provided" in the above quoted reference. This means that the object b is zero initialized.

To zero-initialize an object or reference of type T means:

  • if T is a (possibly cv-qualified) non-union class type, each non-static data member and each base-class subobject is zero-initialized and padding is initialized to zero bits;

This means that the non-static data member a is zero initialized. So it seems that in c++11 the program is well-formed.

t3nsor commented 5 months ago

Aha, so the actual complaint is that these two copy-list-initialization contexts get different treatment per the normative wording:

bool f(C);
bool b = f({});   // [over.match.list], ill-formed if explicit constructor is chosen
C c = {};      // value-initialization -> default-initialization; ignore explicit constructors when forming candidates

I didn't even realize that the normative wording gave these two contexts different treatment, but now that you point it out---yes, that would be a rather odd situation if it were intentional. The philosophy of implicit conversion sequences is that we're more permissive when forming an implicit conversion sequence than when it comes time to actually perform the copy-initialization (e.g. the implicit conversion sequence might involve an inaccessible constructor). In this case it appears to be the other way around: under current wording, the formation of the implicit conversion sequence is ambiguous (in the case of the class A with one explicit default constructor and one non-explicit one) despite the fact that the copy-initialization would be legal.

If I remember correctly, we wanted to consider explicit constructors in f({}) because it's a bad trap to prefer another f overload just because there's an explicit (vs. implicit) constructor somewhere in the mix. Note that {} has no type, which make us consider more things in overload resolution.

Also, the paper that added the example did add "if any" (expressly including zero-parameter constructors) to the rule that is taken only for the C c = {} case, not the f({}), so I doubt we were confused back at the time which rule applied here.

Maybe the comment in the example is just a little off?

Are you saying that it was intentional for the C c = {}; case to ignore explicit constructors? But that would mean it follows a different rule from C c = {x}; (which would use [over.match.list]).

jensmaurer commented 5 months ago

Are you saying that it was intentional for the C c = {}; case to ignore explicit constructors?

Yes. [over.match.copy] also ignores explicit constructors, as opposed to admitting them for later ill-formedness when chosen.

But that would mean it follows a different rule from C c = {x};

Yes. We decided that {} should always value-initialize (and not cause an initializer-list constructor to be called) (cf. [dcl.init.list] p3.5 vs. p3.6), and that's a fairly new bug-fix.

Put differently, C c; also ignores explicit constructors; if we say C c = {} is equivalent to that, that's what you get. We can't be consistent both ways.

t3nsor commented 5 months ago

Put differently, C c; also ignores explicit constructors; if we say C c = {} is equivalent to that, that's what you get. We can't be consistent both ways.

First of all, that's actually not true. C c; is direct-initialization.

Now, I accept that the design direction was that C c = {}; should always value-initialize and never call an initializer-list constructor, but I dispute the idea that all value-initializations have to have the same behavior. According to current wording, both C c = {}; and C c{}; always value-initialize, but they're not equivalent; the direct-initialization syntax is allowed to call an explicit constructor. This difference is achieved through the wording "in the context of copy-initialization" in [over.match.ctor]/1: whether or not that condition is met affects the behavior.

Similarly, without changing the fact that C c = {}; always value-initializes, we could amend [over.match.ctor] to say that if we have "default-initialization in the context of copy-list-initialization", then all constructors are candidates but the initialization is ill-formed if an explicit constructor is selected. This is actually the behavior that I would expect, as it's more consistent with [over.match.list].

t3nsor commented 5 months ago

Actually, I was wrong; C c; is not direct-initialization, but it can call explicit constructors.

t3nsor commented 5 months ago

Suggested resolution: Edit [over.match.ctor]/1 as follows:

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~ (including default-initialization in the context of copy-list-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)~Otherwise, 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. For default-initialization in the context of copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

RealLitb commented 4 months ago

Aha, so the actual complaint is that these two copy-list-initialization contexts get different treatment per the normative wording:

bool f(C);
bool b = f({});   // [over.match.list], ill-formed if explicit constructor is chosen
C c = {};      // value-initialization -> default-initialization; ignore explicit constructors when forming candidates

If I remember correctly, we wanted to consider explicit constructors in f({}) because it's a bad trap to prefer another f overload just because there's an explicit (vs. implicit) constructor somewhere in the mix. Note that {} has no type, which make us consider more things in overload resolution.

I think I agree with the analysis, but the example is, I think, more complicated/unclear than simply saying that "[over.match.list]" is used to resolve the implicit conversion sequence during overload resolution for 'f' in "f({})".

The normative rule that renders actually using the explicit ctor ill-formed is "In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.". In general, when overload resolution allows something but it is "later" ill-formed, it refers to the later initialization of the parameters that will introduce the limitations one way or the other. But actually the explicit constructor is never used by copy list initialization, because the actual initialization for the parameter of "f" and the variable "c" is both the same and use the rule for default initialization.

In my mind, it is not so much the "unclear" / "different" result of overload resolution vs variable initialization, but more that the default constructor behaves differently than a non-default constructor, and the implementation divergence (GCC and Clang keep the non-default-constructor and default-constructor behaviors the same, and declare them ambiguous).

struct A {
    explicit A(int);
    A(float);
};

A a = { 10 }; 

In this example it is clear from the wording that this is ill-formed for choosing A(int), and to me, it seems not clear why this should be different from the case where we use "{}" and making both default constructors, from a programmers point of view. (later part striked, according to the rationale given by Jens above.).

RealLitb commented 4 months ago

Aha, so the actual complaint is that these two copy-list-initialization contexts get different treatment per the normative wording:

bool f(C);
bool b = f({});   // [over.match.list], ill-formed if explicit constructor is chosen
C c = {};      // value-initialization -> default-initialization; ignore explicit constructors when forming candidates

I didn't even realize that the normative wording gave these two contexts different treatment, but now that you point it out---yes, that would be a rather odd situation if it were intentional. The philosophy of implicit conversion sequences is that we're more permissive when forming an implicit conversion sequence than when it comes time to actually perform the copy-initialization (e.g. the implicit conversion sequence might involve an inaccessible constructor). In this case it appears to be the other way around: under current wording, the formation of the implicit conversion sequence is ambiguous (in the case of the class A with one explicit default constructor and one non-explicit one) despite the fact that the copy-initialization would be legal.

It's worse if the default constructors during the formation of the implicit conversion sequences are non-ambiguous and choose the explicit candidate (I don't know enough of modern C++ to rule that out. Constraints come to mind, may they order the default constructor overloads suitably?). Then the implicit conversion sequence is non-ambiguous and well-formed, because the copy list initialization which it is supposed to model will not select it, but a non-explicit constructor.

Testcase:

template<typename T>
struct A {
  explicit A() requires true { }
  A()  { }
};

// ...
void f(A<int> a);
f({});

The ICS considers both, and the copy list init only considers the non-explicit one.

jensmaurer commented 4 months ago

The normative rule that renders actually using the explicit ctor ill-formed is "In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed."

For the particular example, no, because two constructors enter the recursive overload resolution that can't be differentiated. We don't even get to the point where we would have chosen a single candidate to be able to check its explicitness.

jensmaurer commented 4 months ago

CWG2856

RealLitb commented 4 months ago

The normative rule that renders actually using the explicit ctor ill-formed is "In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed."

For the particular example, no, because two constructors enter the recursive overload resolution that can't be differentiated. We don't even get to the point where we would have chosen a single candidate to be able to check its explicitness.

You are right, I got confused by the comment in your code "[over.match.list], ill-formed if explicit constructor is chosen", making me think that in the example, the explicit constructor is chosen.

I primarily wanted to point out that this rule can easily be read two ways:

In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

[Note 1: This differs from other situations ([over.match.ctor], [over.match.copy]), where only converting constructors are considered for copy-initialization. This restriction only applies if this initialization is part of the final result of overload resolution. — end note]

The note seems to take the interpretation that if the recursive overload resolution picks the explicit constructor, and the function "f" is actually selected, then the program is ill-formed (since it takes an initialization to be part of the overload resolution). However, while some parts of the overload resolution chapter call an implicit conversion sequence "an initialization" (see [over.ics.user]p2), other parts define it as merely "modelling" it (see [over.best.ics.general]p6). A model isn't equivalent to the thing that is modelled. An ICS like said above by someone else is often more permissive, as its purpose is basically just estimating the cost of a parameter initialization. Take Note 3 of p6:

[Note 3: When the parameter has a class type, this is a conceptual conversion defined for the purposes of [over]; the actual initialization is defined in terms of constructors and is not a conversion. — end note]

Therefore my above example with the constraint can be read to be, in my opinion, well-formed without the proposed resolution, because the actual copy list initialization does not chose the explicit constructor. I would not necessarily agree with such interpretation, but it's certainly a very valid one. With the current proposed resolution in CWG2856, it will render the code ill-formed twice. Once by [over.match.list] (because the copy-list-initialization's default initialization will select the explicit constructor previously chosen by the ICS in OR for f) and once by [over.match.ctor] itself.

I think that is a good thing! However, the proposed resolution contradicts what you say the committee previously chose the semantics to be:

Put differently, C c; also ignores explicit constructors; if we say C c = {} is equivalent to that, that's what you get. We can't be consistent both ways.

Therefore, I fear that "fixing" this could some day hit us again.

jensmaurer commented 4 months ago

Put differently, C c; also ignores explicit constructors;

That statement was wrong, as pointed out by @t3nsor .

t3nsor commented 4 months ago

Now that I've looked into the history of this a bit more, here's something I've noticed.

CWG990 originally added the rule that value-initialization takes precedence over calling an initializer-list constructor. Later, CWG1229 supplied the missing language in [over.match.list] to make that also happen when forming an ICS.

This suggests that a simpler resolution is possible because [over.match.list] already has the special case for empty lists: edit [over.match.ctor]/1 as follows, splitting it into two paragraphs:

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 default-initialization in the context of list-initialization, see [over.match.list]. Otherwise, the candidate constructors are determined by the rules below.

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.

(I think ideally the wording would be even more parallel between [dcl.init.list] and [over.ics.list], meaning that value-initialization wouldn't even be its own subbullet... but that would require more extensive surgery which is perhaps not worth the effort. The issue is that we'd need to make sure we preserve the zero-initialization behavior that we get with value-initialization.)

jensmaurer commented 4 months ago

We usually don't defer from one [over.match.*] subclause to another. In particular, the front matter of [over.match.list] lists contexts, but "default init in the context of list-init" is not one of them.

If we wanted to defer to [over.match.list], we should branch to [over.match.list] in [dcl.init.general] p7.1 right away for the special case.