Это пример из стандарта С++ 20 (ISO/IEC 14882:2020), раздел 13.5.4 ([temp.constr.normal]), пункт 1 (выделено мной):
Нормальная форма идентификатора концепта C<A1, A2, ..., An> является нормальной формой выражения ограничения C после замены A1, A2, ..., An на соответствующие параметры шаблона C в отображения параметров в каждом атомарном ограничении. Если любая такая замена приводит к недопустимому типу или выражению, программа неправильно сформирована; диагностика не требуется.
template<typename T> concept A = T::value || true;
template<typename U> concept B = A<U*>;
template<typename V> concept C = B<V&>;
Я понимаю, что C делает программу неправильной (и почему). Однако мне не ясно, приведет ли B к тому, что программа будет неправильно сформирована или нет. В тексте говорится, что нормализация B действительна, но в то же время утверждается, что выражение T::value неправильно сформировано из-за этого типа указателя (что я понимаю). Означает ли это, что действительна только часть нормализации процесса, но сама программа неправильно сформирована на более позднем этапе при проверке T::value? Или программа действительна в любом случае и проверка T::value как-то пропускается/избегается?
Я проверил с помощью Godbolt Compiler Explorer, и GCC, и Clang, похоже, с этим справляются. Тем не менее, поскольку в стандарте сказано, что «диагностика не требуется», это не очень помогает.
Концепция B действительна, так как вы можете передать указатель на концепцию A. Внутри самой A указатель не может получить доступ к ::value, но это, согласно спецификации, [temp.constr.atomic], не будет рассматриваться как ошибка, а скорее как false, тогда || true в понятии A составит полное выражение true.
Попытка оценить неправильно сформированное выражение, не требующее диагностики, с использованием static_assert не обязательно поможет ответить на вопрос, является ли выражение допустимым или нет, поскольку компилятору не требуется сбой static_assert на неправильно сформированном- выражение, не требующее диагностики.
Обратите внимание, что каждое нормализованное ограничение состоит из 2 частей: Атомарное ограничение и связанное сопоставление параметров.
Давайте разделим каждое ограничение на эти две части для ваших трех примеров концепций:
В вашем примере нормализованная форма понятия A будет дизъюнкцией этих двух ограничений:
X::value
X ↦ T
true
Нормализованная форма понятия B будет дизъюнкцией этих двух ограничений:
X::value
X ↦ U*
true
И нормализованная форма понятия C будет дизъюнкцией этих двух ограничений:
X::value
X ↦ V&*
true
Формирование сопоставления параметров для атомарного выражения очень просто:
По умолчанию атомарное выражение всегда начинается с сопоставления параметров идентификации (т. е. без изменений типа):
13.5.4 Нормализация ограничений [[temp.constr.normal]] (1) Нормальная форма выражения E — это ограничение, которое определяется следующим образом: [...] (1.5) Нормальной формой любого другого выражения E является атомарное ограничение, выражением которого является E, а сопоставление параметров является отображением идентичности.
И единственный способ получить сопоставление параметров, не являющихся идентификаторами, — это назвать другое понятие в рамках ограничения:
13.5.4 Нормализация ограничений [[temp.constr.normal]] (1.4) Нормальная форма идентификатора концепта C<A1, A2, ..., An> — это нормальная форма выражения ограничения C после замены A1, A2, ..., An на соответствующие параметры шаблона C в сопоставлениях параметров в каждом атомарном ограничении. [...]
Вот несколько примеров:
template<class T> constexpr bool always_true = true;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U (identity)
template<class U> concept Foo = Base<U>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V::type
template<class V> concept Bar = Base<typename V::type>;
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&&
template<class W> concept Baz = Base<W&&>;
Что возвращает нас к вашему исходному цитируемому разделу:
13.5.4 Нормализация ограничений [[temp.constr.normal]] (1.4) Нормальная форма идентификатора концепта C<A1, A2, ..., An> — это нормальная форма выражения ограничения C после замены A1, A2, ..., An на соответствующие параметры шаблона C в сопоставлениях параметров в каждом атомарном ограничении. Если любая такая замена приводит к недопустимому типу или выражению, программа неправильно сформирована; диагностика не требуется.
Обратите внимание, что выделенный оператор применяется только к сопоставлению параметров, а не к самому атомарному выражению.
Обратите внимание, что фактический тип, который заменяется на V, не имеет значения на этапе нормализации; единственное, что имеет значение, это то, что само отображение образует недопустимый тип или выражение.
Вот еще несколько примеров:
template<class T> constexpr bool always_true = true;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ T (identity)
template<class T> concept Base = always_true<T>;
// well-formed
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ U::type
template<class U> concept Foo = Base<typename U::type>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ V*::type
template<class V> concept Bar = Foo<V*>;
// ill-formed, ndr (invalid parameter mapping)
// Atomic constraint: always_true<X>
// Parameter mapping: X ↦ W&*
template<class W> concept Baz = Foo<W&>;
Чтобы ответить на вопрос, когда ваша программа получает неверный формат ndr, нам нужно установить порядок, в котором происходят события во время компиляции.
Когда определяются ограничения связанной декларации ИЛИ оценивается значение концепции, происходит нормализация ограничений. Это дается:
13.5.4 Нормализация ограничений [[temp.constr.normal]] [Note 1] Нормализация ограничений-выражений выполняется при определении связанных ограничений объявления и при оценке значения id-выражения, именующего специализацию концепта.
Именно здесь ваша программа станет неправильно сформированной, ndr, если сопоставление параметров образует недопустимый тип или выражение.
После того, как ограничения были нормализованы, фактический тип будет заменен ограничениями:
13.5.2.3 Атомарные ограничения [[temp.constr.atomic]] (3) Чтобы определить, выполняется ли атомарное ограничение, сопоставление параметров и аргументы шаблона сначала подставляются в его выражение. Если подстановка приводит к недопустимому типу или выражению, ограничение не выполняется.
Обратите внимание, что на этом этапе допускается формирование недопустимых типов или выражений — если это так, то результатом ограничения будет просто false.
Итак, чтобы ответить на ваши вопросы:
Означает ли это, что действительна только часть нормализации процесса, но сама программа неправильно сформирована на более позднем этапе при проверке T::value?
Понятия A и B правильно сформированы. Концепция C неправильно сформирована, ndr в процессе нормализации. Фактическое атомарное ограничение T::value в этом случае не имеет значения; это может быть просто always_true<T>.
Или программа действительна в любом случае и проверка T::value каким-то образом пропускается/избегается?
Программа действительна до тех пор, пока концепция C никогда не нормализуется. т. е. его явное вычисление или использование в качестве ограничения сделало бы вашу программу неправильно сформированной, ndr.
Пример:
// evaluates concept C
// -> results in normalization of C
// -> ill-formed, ndr
static_assert(C</* something */>);
template<C T>
void foo() {}
// constraints of foo will be determined
// -> results in normalization of C
// -> ill-formed, ndr
foo</* something */>();
Сопоставление учитывает только параметры шаблона, которые появляются в пределах ограничения, поэтому сопоставление true пусто.
@Т.С. спасибо, я полностью пропустил эту часть в 13.5.2.3 (1). Я обновил свой пост, теперь он должен быть правильным :)
«мне не ясно, приведет ли B к тому, что программа будет неправильно сформирована или нет». Это прямо говорит, что она действительна; Что еще тебе нужно?