Концептуальный wish-you-happy-debug

от автора

На эти грабли я чуть не наступил (но не наступил!) в рабочем коде, когда захотел прикрутить концепты. Просто задумался о последствиях, проверил на дистиллированном коде, — и да, оно стреляет. Поэтому предлагаю вам в качестве упражнения по ненормальному 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 ошибки. Но в первом случае он был прав, а во втором — ошибся! Стоит удалить первый ассерт, и он тоже перестанет выдавать диагностику.

И вот это уже — БУУ!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Нашли решение загадки?

18.18% Да, это было очевидно / знали раньше2
0% Да, хотя это было неожиданно0
18.18% Нет, сломали голову2
63.64% Нет и не пытались, сразу посмотрели отгадку7

Проголосовали 11 пользователей. Воздержались 3 пользователя.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Если вы решили загадку / знали раньше, то насколько ваше решение совпало?

18.18% Да (с несущественными вариациями), уложились в одну строку2
0% Да, хотя и нагромоздили0
0% Нет, нашли совсем другое (поделитесь в комментариях)0
81.82% Не решили и хотите посмотреть статистику опроса )9

Проголосовали 11 пользователей. Воздержался 1 пользователь.

ссылка на оригинал статьи https://habr.com/ru/articles/857744/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *