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

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

Комментарии (5)


  1. stepsoft
    11.11.2024 19:24

    Сталкивался с попдобным при использовании is_detected. Собственно интересная задача на тему ADL и правил инстанцирования шаблонов.


  1. PaRat07
    11.11.2024 19:24

    можно еще сделать вот так:

    template<class T, auto Func = [] {}> concept boo = requires(T x) { f(x); Func(); };

    и оно будет работать и не потребуется писать каждый раз новую чиселку


    1. nickolaym Автор
      11.11.2024 19:24

      Чиселки я привёл лишь затем, чтобы явно показать разные воплощения шаблона.

      На самом деле, противоядие от "the satisfaction result is different for identical atomic constraints" состоит в том, чтобы сделать ограничения не идентичными. Для этого и надо пропихнуть переменный параметр шаблона внутрь.

      // всё ещё ill-formed-опасная версия
      template<class T, int I> concept boo =
        requires(T x) { f(x); }  // одно атомарное ограничение
        && (I != 0);  // другое атомарное ограничение (ну например)
      
      // well-formed
      template<class T, int I> concept boo =
        requires(T x) { f(x); I; };  // одно большое атомарное ограничение

      Трюк с неявным уникальным параметром шаблона - а каждая лямбда уникальна - это дополнительный уровень. В первую очередь, чтобы не писать чиселки. Чтобы каждое воплощение шаблона было новым. Но в любом случае, придётся упоминать его внутри requires.

      Но мне кажется, что это плохая практика для больших программ. Как минимум, это приведёт к размножению воплощений. Сведём с ума сперва компилятор, а затем линкер...

      Разве что как тест на ill-formed (godbolt):

      // основной концепт для работы
      template<class T> concept boo =
        requires(T x) { f(x); };
      
      // проверочное значение "по месту" - уже не концепт, во избежание
      template<class T, auto F = []{}> constexpr bool boo_checker =
        requires(T x) { f(x); F(); };
      
      .....
      
      void g(boo auto x) {
        static_assert(boo_checker<decltype(x)>);
      }
      void g(auto x) {
        static_assert(!boo_checker<decltype(x)>);
      }


  1. Kelbon
    11.11.2024 19:24

    Просто кланг кеширует значения концепта, а гцц каждый раз считает. Довольно очевидное поведение


    1. nickolaym Автор
      11.11.2024 19:24

      В том и прикол, что гцц значение концепта (булевой константы) кеширует в лучшем виде.

      В вот значение атомарного ограничения (но только для данного концепта) он иногда пересчитывает, а иногда кеширует. Переход с false на true он диагностирует, а в обратную сторону - не догадался. Надо будет им зарепортить, уж больно смешной недочёт. Они не предполагали, что ограничение может откатываться.

      Есть разные способы откатить ограничение вида `requires{ f(x); }`.

      • явно запретить наилучшую сигнатуру, `= delete`

      • добавить конкурирующие сигнатуры, чтобы нельзя было выбрать наилучшую

      • какой-то ещё способ я случайно нашёл, когда писал статью, но сейчас что-то не могу вспомнить