Код для поиграться:
https://gcc.godbolt.org/z/oeznrcGTa
clang и gcc ведут себя несколько по-разному, кстати.
Пусть у нас есть два концепта — Ptr и Vec (тип является голым указателем и вектором, соответственно).
Напишем три шаблона foo(const T&).
А потом попробуем вызвать для вектора / указателя на вектор / вектора векторов.
#include <iostream>
#include <vector>
template<class T> constexpr bool is_vector = false;
template<class T> constexpr bool is_vector<std::vector<T>> = true;
template<class T> concept Vec = is_vector<T>;
template<class T> constexpr bool is_pointer = false;
template<class T> constexpr bool is_pointer<T*> = true;
template<class T> concept Ptr = is_pointer<T>;
template<class T> void foo(T ) { std::cout << "def) " << __PRETTY_FUNCTION__ << std::endl; }
template<Ptr T> void foo(T t) { std::cout << "ptr) " << __PRETTY_FUNCTION__ << std::endl; foo(*t); }
template<Vec T> void foo(T t) { std::cout << "vec) " << __PRETTY_FUNCTION__ << std::endl; foo(t[0]); }
int main() {
std::vector v { 1 };
auto pv = &v;
auto ppv = &pv;
std::vector vv {{ v }};
std::vector vvv {{ vv }};
foo(ppv);
std::cout << std::endl;
foo(v);
std::cout << std::endl;
foo(vvv);
}
В зависимости от того, что именно и в каком порядке мы напишем в main, получим разные результаты — включая ошибку линковки!
В чём тут проблема: определение foo<Ptr> видит только объявления foo<class> и само себя, естественно, — а foo<Vec> ещё не видит.
Поэтому foo(vector<int>) оказывается неоднозначным.
С одной стороны, компилятор инстанцирует его из foo(vector<int>*) как foo<class=vector<int>>;
с другой стороны, из main и/или из foo<Vec> — как foo<Vec=vector<int>>.
Ограничения не входят в манглированное имя (мы можем объявить параметр как концепт, или можем выписать requires, — эффект будет тот же).
Поэтому компилятор не различает эти функции, и мы отхватываем нарушение ODR.
Он или берёт ранее инстанцированную функцию из своего кеша, или принудительно создаёт ещё один объект в объектном файле, удивляя линкера. А при оптимизациях, подозреваю, начнёт хаотически инлайнить / вызывать что бог на душу положит.
Как исправить программу — очевидно. Надо выписать объявления до определений. (Минимально достаточно в данном случае вытащить третье объявление перед вторым определением).
Или, как вариант, запихнуть шаблоны функций в class scope, — там правила видимости другие, все видят всех сразу.
Вопрос в другом.
ЧТО ЭТО?
— дефект стандарта
— дефект правил манглирования имён
— или тут по стандарту должно было возникнуть ODR, и спрос исключительно с клиентского кода?