﻿# C\+\+20: удивить линкер четырьмя строчками кода

Представьте себе, что вы студент, изучающий современные фичи C\+\+\. И вам дали задачу по теме concepts/constraints\. У преподавателя, конечно, есть референсное решение "как правильно", но для вас оно неочевидно, и вы навертели гору довольно запутанного кода, который всё равно не работает\. \(И вы дописываете и дописываете всё новые перегрузки и специализации шаблонов, покрывая всё новые и новые претензии компилятора\)\.


> Мы опубликовали и перевели эту статью с разрешения правообладателя\\\. Автор статьи – Николай Меркин\\\. \[Оригинал\]\(https://habr\.com/ru/post/561974/\) опубликован на сайте Habr\\\.

А теперь представьте себе, что вы — преподаватель, который увидел эту гору, и захотел помочь студенту\. Вы стали упрощать и упрощать его код, и даже тупо комментировать куски юнит\-тестов, чтобы оно хоть как\-то заработало\.\.\. А оно всё равно не работает\. Причём, в зависимости от порядка юнит\-тестов, выдаёт разные результаты или вообще не собирается\. Где\-то спряталось неопределённое поведение\. Но какое?

![0899_C++20_surprise_linker_ru/image1.png](https://import.viva64.com/docx/blog/0899_C++20_surprise_linker_ru/image1.png)

Сперва преподаватель \(то есть, я\) минимизировал код вот до такого: [https://gcc\.godbolt\.org/z/TaMTWqc1T](https://gcc.godbolt.org/z/TaMTWqc1T)

```cpp
// пусть у нас есть концепты указателя и вектора
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](https://gcc.godbolt.org/z/47qhYv6q4)

```cpp
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?

Если мы перепишем код вот так:

```cpp
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](https://gcc.godbolt.org/z/4sar6W6Kq)

```cpp
// добавим второй аргумент 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](https://gcc.godbolt.org/z/PsdhsG6Wr)

```cpp
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](http://rsdn.org/forum/cpp/8020137.flat), подсказал ещё более минималистичный код\!

[Всего 4 строки](https://gcc.godbolt.org/z/qM8xYKfqe):

```cpp
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 не учитывал параметры шаблона, если те не влияли на тип функции\.\.\.

И, судя по ответам разработчиков, не только не умеют, но и [не хотят](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=100825)\. Отмазка: "если в разных точках программы одинаковые обращения к шаблону резолвятся по\-разному, такая программа ill\-formed, никакой диагностики при этом не нужно" \(однако, ill\-formed означает "не скомпилируется", а не "скомпилируется как попало"\.\.\.\)

Проблема [известна с 2017 года](https://github.com/itanium-cxx-abi/cxx-abi/issues/24), но прогресса пока нет\.

Так что живите с этим\. И не забывайте объявлять взаимно\-рекурсивные функции до определений\. А если увидите странные ошибки линкера, то хотя бы будете понимать, из\-за чего они возникают\. \(А если компилятор будет инлайнить наобум, — ну, тогда не повезло\)\.