Это пример из стандарта С++ 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&>;
Нормализация выражения ограничения B действительна и приводит к
T::value
(с отображениемT -> U*
) Vtrue
(с пустым отображением), несмотря на то, что выражениеT::value
неправильно сформировано для типа указателяT
. Нормализация выражения ограничения C приводит к неправильному формату программы, потому что она формирует недопустимый типV&*
в сопоставлении параметров.
Я понимаю, что C делает программу неправильной (и почему). Однако мне не ясно, приведет ли B к тому, что программа будет неправильно сформирована или нет. В тексте говорится, что нормализация B действительна, но в то же время утверждается, что выражение T::value
неправильно сформировано из-за этого типа указателя (что я понимаю). Означает ли это, что действительна только часть нормализации процесса, но сама программа неправильно сформирована на более позднем этапе при проверке T::value
? Или программа действительна в любом случае и проверка T::value
как-то пропускается/избегается?
Я проверил с помощью Godbolt Compiler Explorer, и GCC, и Clang, похоже, с этим справляются. Тем не менее, поскольку в стандарте сказано, что «диагностика не требуется», это не очень помогает.
GCC и Clang отличаются в остальных случаях. Однако неясно, может ли static_assert
показать, что выражение неправильно сформировано, если это выражение IFNDR. Случай нормализованного int&*
в ссылке выше, в строке 9, интересен, так как и Clang, и GCC принимают его как действительный или, по крайней мере, не отвергают.
Концепция B действительна, так как вы можете передать указатель на концепцию A. Внутри самой A указатель не может получить доступ к ::value
, но это, согласно спецификации, [temp.constr.atomic], не будет рассматриваться как ошибка, а скорее как false
, тогда || true
в понятии A составит полное выражение true
.
Обратите внимание, что если мы передаем int& концепту B, то наш код будет IFNDR, так как B попытается передать A недопустимый тип (int&*
).
Концепция C является IFNDR как есть, поскольку она передает ссылку на B, который пытается передать указатель на эту ссылку на A, и снова тип V&*
недействителен.
Попытка оценить неправильно сформированное выражение, не требующее диагностики, с использованием 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 в сопоставлениях параметров в каждом атомарном ограничении. Если любая такая замена приводит к недопустимому типу или выражению, программа неправильно сформирована; диагностика не требуется.
Обратите внимание, что выделенный оператор применяется только к сопоставлению параметров, а не к самому атомарному выражению.
Вот почему концепция C
в вашем примере имеет неправильный формат, NDR - потому что сопоставление параметров для его атомарных выражений формирует недопустимый тип (указатель на ссылку): X ↦ V&*
Обратите внимание, что фактический тип, который заменяется на 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 к тому, что программа будет неправильно сформирована или нет». Это прямо говорит, что она действительна; Что еще тебе нужно?