Представьте себе, что вы студент, изучающий современные фичи C++. И вам дали задачу по теме concepts/constraints. У преподавателя, конечно, есть референсное решение «как правильно», но для вас оно неочевидно, и вы навертели гору довольно запутанного кода, который всё равно не работает. (И вы дописываете и дописываете всё новые перегрузки и специализации шаблонов, покрывая всё новые и новые претензии компилятора).
А теперь представьте себе, что вы — преподаватель, который увидел эту гору, и захотел помочь студенту. Вы стали упрощать и упрощать его код, и даже тупо комментировать куски юнит-тестов, чтобы оно хоть как-то заработало… А оно всё равно не работает. Причём, в зависимости от порядка юнит-тестов, выдаёт разные результаты или вообще не собирается. Где-то спряталось неопределённое поведение. Но какое?
Сперва преподаватель (то есть, я) минимизировал код вот до такого: https://gcc.godbolt.org/z/TaMTWqc1T
// пусть у нас есть концепты указателя и вектора template<class T> concept Ptr = requires(T t) { *t; }; template<class T> concept Vec = requires(T t) { t.begin(); t[0]; }; // и три перегрузки функций, рекурсивно определённые друг через друга template<class T> void f(T t) { // (1) std::cout << "general case " << __PRETTY_FUNCTION__ << std::endl; } template<Ptr T> void f(T t) { // (2) std::cout << "pointer to "; f(*t); // допустим, указатель не нулевой } template<Vec T> void f(T t) { // (3) std::cout << "vector of "; f(t[0]); // допустим, вектор не пустой } // и набор тестов (в разных файлах) int main() { std::vector<int> v = {1}; // тест А f(v); // или тест Б f(&v); // или тест В f(&v); f(v); // или тест Г f(v); f(&v); }
Мы ожидаем, что
-
f(v) выведет «vector of general case void f(T) [T=int]»
-
f(&v) выведет «pointer to vector of general case void f(T) [T=int]»
А вместо это получаем
-
А: «vector of general case void f(T) [T=int]»
-
Б: «pointer of general case void f(T) [T=std::vector<int>]» — ?
-
В: clang выводит
«pointer to general case void foo(T) [T = std::vector<int>]» — как в случае с Б
«general case void foo(T) [T = std::vector<int>]», — не так, как А!
gcc — даёт ошибку линкера -
Г: clang и gcc дают ошибку линкера
Что здесь не так?!
А не так здесь две вещи. Первая — это то, что из функции (2) видны объявления только (1) и (2), поэтому результат разыменования указателя вызывается как (1).
Без концептов и шаблонов это тоже прекрасно воспроизводится: https://gcc.godbolt.org/z/47qhYv6q4
void f(int x) { std::cout << "int" << std::endl; } void g(char* p) { std::cout << "char* -> "; f(*p); } // f(int) void f(char x) { std::cout << "char" << std::endl; } void g(char** p) { std::cout << "char** -> "; f(**p); } // f(char) int main() { char x; char* p = &x; f(x); // char g(p); // char* -> int g(&p); // char** -> char }
В отличие от инлайн-определений функций-членов в классе, где все объявления видны всем, — определение свободной функции видит только то, что находится выше по файлу.
Из-за этого, кстати, для взаимно-рекурсивных функций приходится отдельно писать объявления, отдельно (ниже) определения.
Ладно, с этим разобрались. Вернёмся к шаблонам. Почему в тестах В и Г мы получили нечто, похожее на нарушение ODR?
Если мы перепишем код вот так:
template<class T> void f(T t) {.....} template<class T> void f(T t) requires Ptr<T> {.....} template<class T> void f(T t) requires Vec<T> {.....}
то ничего не изменится. Это просто другая форма записи. Требование соответствия концепту можно записать и так, и этак.
Но вот если прибегнем к старому доброму трюку SFINAE, https://gcc.godbolt.org/z/4sar6W6Kq
// добавим второй аргумент char или int - для разрешения неоднозначности template<class T, class = void> void f(T t, char) {.....} template<class T> auto f(T t, int) -> std::enable_if_t<Ptr<T>, void> {.....} template<class T> auto f(T t, int) -> std::enable_if_t<Vec<T>, void> {.....} ..... f(v, 0) ..... ..... f(&v, 0) .....
или ещё более старому доброму сопоставлению типов аргументов, https://gcc.godbolt.org/z/PsdhsG6Wr
template<class T> void f(T t) {.....} template<class T> void f(T* t) {.....} template<class T> void f(std::vector<T> t) {.....}
то всё станет работать. Не так, как нам хотелось бы (рекурсия по-прежнему сломана из-за правил видимости), но ожидаемо (вектор из f(T*) видится как «general case», из main — как «vector»).
Что же ещё с концептами/ограничениями?
Коллективный разум, спасибо RSDN, подсказал ещё более минималистичный код!
template<class T> void f() {} void g() { f<int>(); } template<class T> void f() requires true {} void h() { f<int>(); }
Функция с ограничениями считается более предпочтительной, чем функция без них. Поэтому g() по правилам видимости выбирает из единственного варианта, а h() — из двух выбирает второй.
И вот этот код порождает некорректный объектный файл! В нём две функции с одинаковыми декорированными именами.
Оказывается, современные компиляторы (clang ≤ 12.0, gcc ≤ 12.0) не умеют учитывать requires в декорировании имён. Как когда-то старый глупый MSVC6 не учитывал параметры шаблона, если те не влияли на тип функции…
И, судя по ответам разработчиков, не только не умеют, но и не хотят. Отмазка: «если в разных точках программы одинаковые обращения к шаблону резолвятся по-разному, такая программа ill-formed, никакой диагностики при этом не нужно» (однако, ill-formed означает «не скомпилируется», а не «скомпилируется как попало»…)
Проблема известна с 2017 года, но прогресса пока нет.
Так что живите с этим. И не забывайте объявлять взаимно-рекурсивные функции до определений. А если увидите странные ошибки линкера, то хотя бы будете понимать, из-за чего они возникают. (А если компилятор будет инлайнить наобум, — ну, тогда не повезло).
ссылка на оригинал статьи https://habr.com/ru/post/561974/
Добавить комментарий