Overload resolution for templated conversion operators

largest_prime_is_463035818

This code:

#include <iostream>
template <typename T>
void print_type(){ std::cout << __PRETTY_FUNCTION__ << '\n'; }

template <typename T>
struct foo {
    operator T(){
        std::cout << "T conversion ";         
        print_type<T>();
        return {};
    }
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }        
};

int main(void) {
    unsigned a  = 20;
    foo<uint8_t> z;
    auto y = z*a;
}

compiles (with gcc 9.1.0) and prints:

ANY conversion void print_type() [with T = int]

On the other hand, if I remove the operator T (which is not called above):

template <typename T>
struct bar {
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }        
};

int main(void) {
    unsigned a  = 20;
    bar<uint8_t> z;
    auto y = z*a;
}

I get an error:

prog.cc: In function 'int main()':
prog.cc:19:15: error: no match for 'operator*' (operand types are 'bar<unsigned char>' and 'unsigned int')
   19 |     auto y = z*a;
      |              ~^~
      |              | |
      |              | unsigned int
      |              bar<unsigned char>

At first I was surprised that foorequires an operator T for operator S to be choosen. However, is gcc even right here? Clang 8.0 complains with

prog.cc:24:15: error: use of overloaded operator '*' is ambiguous (with operand types 'foo<uint8_t>' (aka 'foo<unsigned char>') and 'unsigned int')
    auto y = z*a;
             ~^~
prog.cc:24:15: note: built-in candidate operator*(float, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(double, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(long double, unsigned int)
prog.cc:24:15: note: built-in candidate operator*(__float128, unsigned int)
[...]

... the list continues with all kinds of candidates.

Why does the first example compile with gcc but not with clang? Is this a bug in gcc?

Brian

This is a real tour-de-force of the standard.

When foo<uint8_t> is instantiated, the specialization looks like this:

struct foo<uint8_t> {
    operator uint8_t(){
        std::cout << "T conversion ";         
        print_type<uint8_t>();
        return {};
    }
    template <typename S>
    operator S(){
        std::cout << "ANY conversion ";
        print_type<S>();
        return {};
    }
};

In other words, the class contains a non-template conversion operator to uint8_t, and a conversion operator template to arbitrary S.

When the compiler sees z * a, [over.match.oper]/(3.3) defines the set of built-in candidates:

For the operator ,, the unary operator &, or the operator ->, the built-in candidates set is empty. For all other operators, the built-in candidates include all of the candidate operator functions defined in 16.6 that, compared to the given operator, * have the same operator name, and * accept the same number of operands, and * accept operand types to which the given operand or operands can be converted according to 16.3.3.1, and * do not have the same parameter-type-list as any non-member candidate that is not a function template specialization.

The built-in candidates defined in 16.6/13 for operator* are:

For every pair of promoted arithmetic types L and R, there exist candidate operator functions of the form

LR operator*(L, R);
// ...

Clang is printing out the full list of such built-in candidates. Presumably GCC agrees with this list. Now overload resolution must be applied to select the one to "call". (Of course, the built-in operator* is not a real function, so "calling" it just means converting the arguments to the "parameter" types and then executing the built-in multiplication operator.) Obviously, the best viable candidate would have R be unsigned int so that we get an exact match for the second argument, but what about the first argument?

For a given L, the compiler must recursively apply overload resolution with the candidates described in [over.match.conv] to determine how to convert foo<uint8_t> to L:

Under the conditions specified in 11.6, as part of an initialization of an object of non-class type, a conversion function can be invoked to convert an initializer expression of class type to the type of the object being initialized. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1 T” is the type of the object being initialized, and “cv S” is the type of the initializer expression, with S a class type, the candidate functions are selected as follows:

  • The conversion functions of S and its base classes are considered. Those non-explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T via a standard conversion sequence (16.3.3.1.1) are candidate functions. For direct-initialization, those explicit conversion functions that are not hidden within S and yield type T or a type that can be converted to type T with a qualification conversion (7.5) are also candidate functions. Conversion functions that return a cv-qualified type are considered to yield the cv-unqualified version of that type for this process of selecting candidate functions. Conversion functions that return “reference to cv2 X” return lvalues or xvalues, depending on the type of reference, of type “cv2 X” and are therefore considered to yield X for this process of selecting candidate functions.

The argument list has one argument, which is the initializer expression. [ Note: This argument will be compared against the implicit object parameter of the conversion functions. —end note ]

So one candidate to convert foo<uint8_t> to L is to call operator uint8_t and then do whatever standard conversion is necessary to convert uint8_t to L. The other candidate is to call operator S, but S must be deduced as specified in [temp.deduct.conv]:

Template argument deduction is done by comparing the return type of the conversion function template (call it P) with the type that is required as the result of the conversion (call it A; see 11.6, 16.3.1.5, and 16.3.1.6 for the determination of that type) as described in 17.8.2.5. ...

Thus, the compiler will deduce S = L.

To select whether to call operator uint8_t or operator L, the overload resolution process is used with the foo<uint8_t> object as the implied object argument. Since the conversion from foo<uint8_t> to the implied object argument type is just the identity conversion in both cases (as both operators are direct members with no cv-qualification), the tie-breaker rule [over.match.best]/(1.4) must be used:

the context is an initialization by user-defined conversion (see 11.6, 16.3.1.5, and 16.3.1.6) and the standard conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity being initialized) is a better conversion sequence than the standard conversion sequence from the return type of F2 to the destination type ...

Thus, the compiler will always select operator L over operator uint8_t in order to get the identity conversion from the result of the conversion operator to L (unless L itself is uint8_t, but that can't happen because L must be a promoted type).

Thus, for each possible L, to "call" operator* LR(L, R), the implicit conversion sequence required for the first argument is the user-defined conversion of calling operator L. When comparing operator*s with different L's, there is no way for the compiler to decide which one is best: in other words, should it call operator int to call operator*(int, unsigned int), or should it call operator unsigned int to call operator*(unsigned int, unsigned int), or should it call operator double to call operator*(double, unsigned int), and so on? All are equally good options, and the overload is ambiguous. Thus, Clang is right and GCC has a bug.

Este artículo se recopila de Internet, indique la fuente cuando se vuelva a imprimir.

En caso de infracción, por favor [email protected] Eliminar

Editado en
0

Déjame decir algunas palabras

0Comentarios
Iniciar sesiónRevisión de participación posterior

Artículos relacionados

Templated overloaded operator resolution, no implicit cast

Typescript: Function overload resolution for number | ArrayBuffer

Overload resolution for two functions taking parameter packs

How to avoid local lambdas shadow overload resolution?

Overload resolution issue for generic method with constraints

Bug in templated conversion operator in GCC: Workaround?

Bug in templated conversion operator in GCC: Workaround?

Scope resolution in templated inheritance (possibly what is called mixin)

Overload resolution gets different result between gcc and clang

Overload resolution when an argument is an initializer list and the parameter is a reference

Does C++ guarantee this enum vs int constructor overload resolution?

Overload resolution involving old-style enums and integral types

Overload resolution produces ambiguous call when template parameters added

Correct way to call user defined conversion operator in templated function with MSVC

Forcing C++ to prefer an overload with an implicit conversion over a template

How to resolve Overload resolution ambiguity for context.report within custom lint rules?

Overloaded function and multiple conversion operators ambiguity in C++, compilers disagree

¿Por qué Overload Resolution favorece la función de plantilla sin restricciones sobre una más específica?

Is there a way to define implicit conversion operators in C# for specific versions of generic types?

overload delete [] для массива указателей

Templated class syntax errors

"decay" a templated alias

Rcpp: Constructing templated functions

What are the operators ==> and =?>?

No match operators[]

Logical operators (and or operators) using R

Emulating static constructors for templated classes

Using std::function on a templated function

"Templated" -Funktionen für Julia

TOP Lista

CalienteEtiquetas

Archivo