На эти грабли я чуть не наступил (но не наступил!) в рабочем коде, когда захотел прикрутить концепты. Просто задумался о последствиях, проверил на дистиллированном коде, - и да, оно стреляет. Поэтому предлагаю вам в качестве упражнения по ненормальному C++.
Итак. Пусть у нас есть полиморфная (шаблонная, перегруженная, - неважно) функция f(x).
И мы написали концепт, который говорит, что тип может быть аргументом этой функции.
Назовём его fable, то есть, "f-абельный", или, по-русски, "сказка". (Эта сказка будет страшной).
На C++20 это выглядит очень просто и элегантно. (Без requires в виде шаблонной метафункции это тоже делается, но заметно громоздче).
template<class T> concept fable = requires(const T& x) { f(x); };
И попробуем применить его на практике.
struct A{};
struct B{};
void f(A);
static_assert(fable<A>);
static_assert(!fable<B>);
const char* kind(auto x) { return "non-fable"; }
// функция с ограничением имеет приоритет
const char* kind(fable auto x) { return "fable"; }
template<class T> void test() {
T x;
std::cout << kind(x) << std::endl;
}
int main() {
test<A>(); // fable
test<B>(); // non-fable
}
Пока что всё было хорошо... Но вдруг что-то сломалось и пошло не так.
struct C{};
... /* здесь какой-то код */
f(C);
static_assert(fable<C>); // ошибка! fable<C> == false.
Сломав голову, что же там неправильно, напишем второй - точно такой же - концепт!
template<class T> concept fable2 = requires(const T& x) { f(x); };
// и сделаем проверку рядом с тем злосчастным ассертом
static_assert(fable2<C>);
static_assert(!fable<C>); // мы уже знаем, что он false :(
Даже напихаем отладочного вывода в test()
template<class T> void test() {
T x;
bool a = fable<T>;
bool b = fable2<T>;
std::cout << std::boolalpha
<< kind(x) << " "
<< a << " " << b << " " << (a == b ? "ok" : "wtf")
<< std::endl;
}
int main() {
test<A>(); // true true ok
test<B>(); // false false ok
test<C>(); // false true wtf
}
Итак, загадка. Ниже приведён почти полный код (можете поиграть с ним на godbolt).
Wish you happy debug!
#include <iostream>
#include <iomanip>
template<class T> concept fable = requires(const T& x) { f(x); };
template<class T> concept fable2 = requires(const T& x) { f(x); };
template<class T> void test() {
T x;
bool a = fable<T>;
bool b = fable2<T>;
std::cout << std::boolalpha
<< kind(x) << " "
<< a << " " << b << " " << (a == b ? "ok" : "wtf")
<< std::endl;
}
struct A{};
struct B{};
struct C{};
void f(A);
.....
void f(C);
int main() {
test<A>(); // true true ok
test<B>(); // false false ok
test<C>(); // false true wtf
static_assert( fable<A> && fable2<A>);
static_assert(!fable<B> && !fable2<B>);
static_assert(!fable<C> && fable2<C>);
}
Что же такое - весьма невинное, на первый взгляд, - притаилось на месте многоточия?
Клянусь, что это ничего похожего на традиционное заподло!
#define true false // wish you happy debug
Попробуйте сами придумать минимальный код, прежде чем читать отгадку дальше.
Скрытый текст
Буквально одна строчка.
static_assert(!fable<C>);
Я же говорил! Выглядит совершенно невинно. И, что самое удивительное, выглядит справедливо. Ведь сразу после объявления типа C у нас ещё нет функции f(C). А значит, и требование для концепта не выполняется.
Зато именно в этом месте мы инстанцировали шаблонную булеву константу fable<C> (а концепты - это шаблоны булевых констант со специальным синтаксисом и семантикой). И ниже по коду уже пользуемся тем значением, которое она принимает.
Это касается абсолютно всех шаблонов - и классов, и функций, и обычных констант.
В ходе обсуждения на RSDN подсветили смежную проблему. Расскажу о ней тоже в виде страшной сказки-подсказки. Ладно, уже без спойлера, - вы ведь успели поломать голову самостоятельно (или уже посмотрели отгадку)?
Для начала, - чтобы не копипастить концепт, сделаем его параметризуемым. И будем проверять его значения в разных точках кода (опять же, можете проверить на godbolt):
template<class T, int I> concept boo = requires(T x) { f(x); };
struct D{};
// ещё нет функции f(D)
static_assert(!boo<D, 1>);
void f(auto) {}
// а теперь она есть!
static_assert( boo<D, 2>);
void f(D) = delete;
// а теперь её снова нет!
static_assert(!boo<D, 3>);
int main() {}
Стандарт говорит про концепты:
If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required.
Очевидно, что в коде выше - одинаковые атомарные ограничения дали разный результат. И вот clang (trunk на момент написания статьи 19.1.0) воспользовался тем, что "no diagnostic required" и скомпилировал как смог.
А gcc - воспользовался тем, что "диагностика не требуется" не значит, что она запрещена. И показал 2 ошибки. Но в первом случае он был прав, а во втором - ошибся! Стоит удалить первый ассерт, и он тоже перестанет выдавать диагностику.
И вот это уже - БУУ!