cplusplus / CWG

Core Working Group
23 stars 7 forks source link

[over.match.list] Does use of explicit ctor in copy-list-initialization makes the program ill-formed or only the initialization when it is the final result of overload resolution #503

Open ranaanoop opened 4 months ago

ranaanoop commented 4 months ago

Full name of submitter: Anoop Rana

Reference (section label): [over.match.list]

Link to reflector thread (if any): https://stackoverflow.com/a/77979372/12002570

Issue description:

The following program is rejected by gcc but both clang and msvc accepts. AFAIK it is invalid per the current wording. But I think it should be made valid because overload resolution usually discards the non-viable options instead of making the program ill-formed. The reason is explained in this answer and for completeness here is the same:

#include <type_traits>
#include <vector>

struct A {
    A();
};
static_assert(std::is_convertible_v<double, A> == false);
static_assert(std::is_convertible_v<A, double> == false);

void func(std::vector<double> values);
void func(std::vector<A> as);

int main() {
    func({ 4.2 });
}

Step 1

First let us consider the overload resolution for the call func({4.2}) to the first overload func(std::vector<double>).

Note note that func({ 4.2 }) is copy-initialization:

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 other than by a designated-initializer-clause ([dcl.init.aggr]), is called copy-initialization.

Now we move onto dcl.init.general#16 to see that this will use list initialization:

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]).

So from dcl.init.list we also see that this is copy-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. Direct-initialization that is not list-initialization is called direct-non-list-initialization.

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

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

  • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

This means that overload resolution is done with std::vector's for the argument {4.2} and the best one will be choosen.

So we move onto over.match.list:

When objects of non-aggregate class type T are list-initialized such that [dcl.init.list] specifies that overload resolution is performed according to the rules in this subclause or when forming a list-initialization sequence according to [over.ics.list], overload resolution selects the constructor in two phases:

  • If the initializer list is not empty or T has no default constructor, overload resolution is first performed where the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.
  • Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

Note that since an initializer list ctor was found, so overload resolution won't be performed again. This in turn means that the initializer list ctor is the choosen option when matching func({4.2}) against the first overload func(std::vector<double>)

Step 2

Now we see how func({4.2}) matches against the second overload func(std::vector<A>).

In this case, almost all the steps are same(as in the last case) except that this time the initializer list ctor std::vector(std::initializer_list<A>) is not viable and so the statement if no viable initializer-list constructor is found, overload resolution is performed again is satisfied and so

Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

This means that this time, the std::size_t argument ctor of std::vector will be choosen. But note that this ctor of std::vector is explicit and we have:

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

Thus the selection of size_t argument ctor of std::vector makes the program ill-formed.

jensmaurer commented 4 months ago

The note at the end of [over.match.list] clarifies that the ill-formedness only applies if we'll end up with the so-constructed sequence at the very end:

"This restriction only applies if this initialization is part of the final result of overload resolution."

Please post a bug report to gcc.

t3nsor commented 4 months ago

This is actually a duplicate of CWG2525.

GCC takes the point of view that the explicitness of the constructor doesn't affect the formation of the ICS, which makes the overload resolution ambiguous. Clang and MSVC take the point of view that if the ICS would be ill-formed due to an explicit constructor being used for copy-list-initialization, then the ICS doesn't exist.

ranaanoop commented 4 months ago

@jensmaurer A gcc bug was posted for the same but they've closed that bug saying that gcc is right in rejecting the code.

The note at the end of [over.match.list] clarifies that the ill-formedness only applies if we'll end up with the so-constructed sequence at the very end:

But notes are non-normative? Shouldn't this be normative.

jensmaurer commented 4 months ago

@t3nsor , I can understand those differences in viewpoint for the particular special "ill-formed" rule we have here. Why is that related to CWG2525, where we discuss the wording for forming ICS in general?

jensmaurer commented 4 months ago

@ranaanoop , Ah, so the rejection by gcc is not because of the special "ill-formed" rule, but because of ambiguous overload resolution before we (presumably) hit the ill-formed rule.

t3nsor commented 4 months ago

CWG2525 is all about the fact that implicit conversion sequences are able to be formed under some situations where the actual copy-initialization would be ill-formed (assuming, of course, that the function with which that ICS is associated doesn't get selected), but the standard doesn't explain exactly which types of ill-formedness we let slip when forming an ICS.

ranaanoop commented 4 months ago

@jensmaurer That seems to be the case looking at the error(of gcc). I also noted that a deleted answer was posted which also implied/said that the "program" is ill-formed instead of saying that only the "initialization" is ill-formed when matching {4.2} against func(std::vector<A>). Since that answer was deleted by their user I am posting that answer below:

GCC is correct:

Per [over.ics.list]/7, in order to determine the conversion rank for { 4.2 } for each of the two candidate functions, overload resolution according to [over.match.best] is done as if initializing a variable of the function parameters type from = { 4.2 }.

According to [over.match.best], for that nested overload resolution, there are two phases in which constructors are considered. First std::initializer_list constructors and then all constructors. Although it is copy-list-initialization, all constructors are considered in that second phase, not only non-explicit ones. If an explicit constructor is chosen at the end, then the program is ill-formed.

std::vector<A> does not have a viable std::initializer_list constructor, but it does have a constructor with only non-defaulted parameter being std::vector::size_type. This is an integral type to which 4.2 (a double) can be implicitly converted. That constructor is explicit, but as [over.match.list] says, it is considered regardless.

The nested overload resolution therefore succeeds in choosing that constructor for a user-defined conversion sequence. But because it uses an explicit constructor in copy-list-initialization, the program is ill-formed per [over.match.list].

That this may be unintuitive behavior was subject of a CWG issue that was closed as not-a-defect with a remark that the behavior is intended: CWG 1228

Note the emphasis on the "program" is ill-formed. It is not clear to me why the word "ill-formed" is used/chosen in [over.match.list] instead of saying something like "it is not viable". IMO the word ill-formed looks to be too strong here and it seems to imply that if some construct is ill-formed then the whole program will also be ill-formed. So it should be clarified by say making the note given at the end there(that you pointed out) to be normative or just not using the word ill-formed.

jensmaurer commented 4 months ago

But "ill-formed" is intended (when the conversion is actually chosen), as is the overload resolution ambiguity, as can be seen in CWG1228.

jensmaurer commented 4 months ago

Is there anything left between CWG1228 and CWG2525 that is novel for this issue?

ranaanoop commented 4 months ago

@jensmaurer So the note can be made normative editorially? Or do you think there is no need to make it normative?