Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Ambiguous conversion sequence" - what is the purpose of this concept?

In N4659 16.3.3.1 Implicit conversion sequences says

10 If several different sequences of conversions exist that each convert the argument to the parameter type, the implicit conversion sequence associated with the parameter is defined to be the unique conversion sequence designated the ambiguous conversion sequence. For the purpose of ranking implicit conversion sequences as described in 16.3.3.2, the ambiguous conversion sequence is treated as a user-defined conversion sequence that is indistinguishable from any other user-defined conversion sequence [Note: This rule prevents a function from becoming non-viable because of an ambiguous conversion sequence for one of its parameters.] If a function that uses the ambiguous conversion sequence is selected as the best viable function, the call will be ill-formed because the conversion of one of the arguments in the call is ambiguous.

(The corresponding section of the current draft is 12.3.3.1)

What is the intended purpose of this rule and the concept of ambiguous conversion sequence it introduces?

The note supplied in the text states that the purpose of this rule is "to prevent a function from becoming non-viable because of an ambiguous conversion sequence for one of its parameters". Um... What does this actually refer to? The concept of a viable function is defined in the preceding sections of the document. It does not depend on ambiguity of conversions at all (conversions for each argument must exist, but they don't have to be unambiguous). And there seems to be no provision for a viable function to somehow "become non-viable" later (neither because of some ambiguity nor anything else). Viable functions are enumerated, they compete against each other for being "the best" in accordance with certain rules and if there's a single "winner", the resolution is successful. At no point in this process a viable function may (or needs) to turn into a non-viable one.

The example provided within the aforementioned paragraph is not very enlightening (i.e. it is not clear what role the above rule plays in that example).


The question originally popped up in connection with this simple example

struct S
{
  operator int() const { return 0; };
  operator long() const { return 0; };
};

void foo(int) {}

int main()
{
  S s;
  foo(s);
}

Let's just mechanically apply the above rule here. foo is a viable function. There are two implicit conversion sequences from argument type S to parameter type int: S -> int and S -> long -> int. This means that per the above rule we have to "pack" them into a single ambiguous conversion sequence. Then we conclude that foo is the best viable function. Then we discover that it uses our ambiguous conversion sequence. Consequently, per the above rule the code is ill-formed.

This seems to make no sense. The natural expectation here is that S -> int conversion should be chosen, since it is ranked higher than S -> long -> int conversion. All compilers I know follow that "natural" overload resolution.

So, what am I misunderstanding?

like image 853
AnT Avatar asked Jul 23 '19 17:07

AnT


1 Answers

To be viable, there must be an implicit conversion sequence.

The standard could have permitted multiple implicit conversion sequences, but that might make the wording for determining which overload to select more complex.

So the standard ends up defining one and exactly one implicit conversion sequence for each argument to each parameter. In the case where there is ambiguity, the one it uses is the ambiguous conversion sequence.

Once this is done, it no longer has to deal with the possibility of multiple conversion sequences of one argument to one parameter.

Imagine if we where writing this in C++. We might have a few types:

namespace conversion_sequences {
  struct standard;
  struct user_defined;
  struct ellipsis;
}

where each has plenty of stuff in them (skipped here).

As each conversion sequence is one of the above, we define:

  using any_kind = std::variant< standard, user_defined, ellipsis >;

Now, we run into the case of there being more than one conversion sequence for a given argument and parameter. We have two choices at this point.

We could pass around a using any_kinds = std::vector<any_kind> for a given argument,parameter pair, and ensure all of the logic that handles picking a conversion sequence deals with this vector...

Or we could note that the handling of a 1 or more entry vector never looks at the elements in the vector, and it is treated exactly like a kind of user_defined conversion sequence, until the very end at which point we generate an error.

Storing that extra state, and having the extra logic, is a pain. We know we don't need that state, and we don't need code to handle the vector. So we just define a sub-type of conversion_sequence::user_defined, and the code after it finds the preferred overload it checks if the resulting overload chosen should generate errors.

While the C++ standard is not (always) implemented in C++ and the wording doesn't have to have a 1:1 relationship with its implementation, writing a robust standard document is a kind of coding, and some of the same concerns apply.

like image 166
Yakk - Adam Nevraumont Avatar answered Sep 27 '22 17:09

Yakk - Adam Nevraumont