Разрешение перегрузки для шаблонных операторов преобразования - PullRequest
2 голосов
/ 27 июня 2019

Этот код:

#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;
}

компилирует (с gcc 9.1.0) и печатает :

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

С другой стороны, если я удалю operator T (который не называется выше):

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;
}

Я получаю ошибку:

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>

Сначала я был удивлен, что foo требует operator T для выбора operator S. Однако gcc даже здесь? Clang 8.0 жалуется с

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

... список продолжается со всеми видами кандидатов.

Почему первый пример компилируется с gcc, а не с clang? Это ошибка в gcc?

1 Ответ

2 голосов
/ 28 июня 2019

Это настоящая поездка по стандарту.

Когда создается экземпляр foo<uint8_t>, специализация выглядит следующим образом:

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 {};
    }
};

Другими словами, класссодержит оператор преобразования без шаблона в uint8_t и шаблон оператора преобразования в произвольный S.

Когда компилятор видит z * a, [over.match.oper] / (3.3) определяетнабор встроенных кандидатов:

Для оператора ,, унарного оператора & или оператора -> набор встроенных кандидатов пуст.Для всех других операторов встроенные кандидаты включают в себя все операторные функции-кандидаты, определенные в 16.6, которые по сравнению с данным оператором * имеют одинаковое имя оператора и * принимают одинаковое количество операндов и * принимают типы операндов длякоторый данный операнд или операнды могут быть преобразованы в соответствии с 16.3.3.1, и * не имеют того же списка параметров типа, что и любой другой кандидат, не являющийся кандидатом, который не является специализацией шаблона функции.Встроенные кандидаты, определенные в 16.6 / 13 для operator*:

Для каждой пары выдвинутых арифметических типов L и R существуют операторные функции-кандидаты вида

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

Clang распечатывает полный список таких встроенных кандидатов.Предположительно GCC согласен с этим списком.Теперь необходимо применить разрешение перегрузки, чтобы выбрать «вызов».(Конечно, встроенная operator* не является реальной функцией, поэтому «вызов» это просто означает преобразование аргументов в типы «параметров», а затем выполнение встроенного оператора умножения.) Очевидно, лучший жизнеспособный кандидат R будет unsigned int, так что мы получим точное совпадение для второго аргумента, но как насчет первого аргумента?

Для данного L ,компилятор должен рекурсивно применить разрешение перегрузки с кандидатами, описанными в [over.match.conv], чтобы определить, как преобразовать foo<uint8_t> в L :

В условиях, указанных в 11.6как часть инициализации объекта неклассового типа может быть вызвана функция преобразования для преобразования выражения инициализатора типа класса в тип инициализируемого объекта.Разрешение перегрузки используется для выбора функции преобразования, которая будет вызвана.Предполагая, что « cv1 T» является типом инициализируемого объекта, а « cv S» является типом выражения инициализатора с классом SТип, функции-кандидаты выбираются следующим образом:

  • Рассматриваются функции преобразования S и его базовые классы.Те неявные функции преобразования, которые не скрыты в S и дают тип T или тип, который можно преобразовать в тип T через стандартную последовательность преобразования (16.3.3.1.1), являются функциями-кандидатами.Для прямой инициализации те функции явного преобразования, которые не скрыты в S и дают тип T или тип, который можно преобразовать в тип T с помощью преобразования квалификации (7.5), также являются функциями-кандидатами.Считается, что функции преобразования, которые возвращают cv-квалифицированный тип, дают cv-неквалифицированную версию этого типа для этого процесса выбора функций-кандидатов.Функции преобразования, которые возвращают «ссылку на cv2 X», возвращают lvalue или xvalues, в зависимости от типа ссылки, типа « cv2 X» и, следовательно, считаются получающимиX для этого процесса выбора функций-кандидатов.

Список аргументов имеет один аргумент, который является выражением инициализатора.[ Примечание: Этот аргумент будет сравниваться с неявным параметром объекта функций преобразования. - Конечная заметка ]

Итак, одна конфетадата для преобразования foo<uint8_t> в L заключается в том, чтобы позвонить operator uint8_t, а затем выполнить любое стандартное преобразование, необходимое для преобразования uint8_t в L .Другой кандидат должен вызвать operator S, но S должно быть выведено, как указано в [temp.deduct.conv]:

Вывод аргумента шаблона выполняется путем сравнения возвращаемого типа преобразованияшаблон функции (назовите его P) с типом, который требуется в результате преобразования (назовите его A; для определения этого типа см. 11.6, 16.3.1.5 и 16.3.1.6), как описано в 17.8..2.5....

Таким образом, компилятор выведет S = L .

Чтобы выбрать, вызывать ли operator uint8_t или operator L , процесс разрешения перегрузки используется с объектом foo<uint8_t> в качестве подразумеваемого аргумента объекта.Поскольку преобразование из foo<uint8_t> в неявный тип аргумента объекта является просто преобразованием идентичности в обоих случаях (так как оба оператора являются прямыми членами без квалификации cv), правило разрыва связей [over.match.best] / (1.4) должны использоваться:

контекст является инициализацией путем пользовательского преобразования (см. 11.6, 16.3.1.5 и 16.3.1.6) и стандартной последовательности преобразования из возвращаемого типа F1для типа назначения (т. е. типа инициализируемого объекта) является лучшей последовательностью преобразования, чем стандартная последовательность преобразования из типа возврата F2 в тип назначения ...

Таким образомкомпилятор всегда будет выбирать operator L более operator uint8_t, чтобы получить преобразование идентификатора из результата оператора преобразования в L (если только L само по себе uint8_t, но этого не может быть, потому что L должен быть повышенным типом).

Таким образом, для каждого возможного L , чтобы "вызвать"«operator* LR(L, R), импликацияЭто последовательность преобразования, требуемая для первого аргумента, является определяемым пользователем преобразованием вызова operator L .При сравнении operator* с различными L , компилятор не может решить, какой из них лучше: другими словами, должен ли он вызывать operator int для вызова operator*(int, unsigned int), или долженон звонит operator unsigned int, чтобы позвонить operator*(unsigned int, unsigned int), или он должен звонить operator double, чтобы звонить operator*(double, unsigned int), и так далее?Все одинаково хорошие варианты, и перегрузка неоднозначна.Таким образом, Clang прав, а GCC содержит ошибку.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...