Включается ли ограничение ограничений только на понятия?

Рассмотрим этот пример:

template <typename T> inline constexpr bool C1 = true;    
template <typename T> inline constexpr bool C2 = true;

template <typename T> requires C1<T> && C2<T> 
constexpr int foo() { return 0; }

template <typename T> requires C1<T> 
constexpr int foo() { return 1; }

constexpr int bar() {
    return foo<int>();
}

Является ли вызов foo<int>() неоднозначным, или ограничение C1<T> && C2<T> означает C1<T>?

Ответы

Ответ 1

Да. Могут быть включены только понятия. Вызов foo<int> неоднозначен, потому что ни одна из деклараций не является "по меньшей мере такой же ограниченной, как" другая ".

Если, однако, C1 и C2 были concept вместо inline constexpr bool s, то объявление foo() которое возвращает 0 было бы по меньшей мере таким же ограниченным, как объявление foo() которое возвращает 1, а вызов foo<int> будет действительным и вернет 0. Это одна из причин предпочесть использовать понятия в качестве ограничений для произвольных булевых константных выражений.


Фон

Причина этой разницы (понятия subsume, произвольные выражения нет) лучше всего выражается в сопоставлении семантических ограничений для понятий, которые стоит полностью прочитать (я не буду воспроизводить все аргументы здесь). Но, взяв пример из статьи:

namespace X {
  template<C1 T> void foo(T);
  template<typename T> concept Fooable = requires (T t) { foo(t); };
}
namespace Y {
  template<C2 T> void foo(T);
  template<typename T> concept Fooable = requires (T t) { foo(t); };
}

X::Fooable эквивалентен Y::Fooable несмотря на то, что они означают совершенно разные вещи (в силу того, что они определены в разных пространствах имен). Такая случайная эквивалентность проблематична: набор перегрузки с функциями, ограниченными этими двумя понятиями, будет неоднозначным.

Эта проблема усугубляется, когда одна концепция попутно усовершенствует другие.

namespace Z {
  template<C3 T> void foo(T);
  template<C3 T> void bar(T);
  template<typename T> concept Fooable = requires (T t) {
    foo(t);
    bar(t);
  };
}

Набор перегрузки, содержащий отдельных жизнеспособных кандидатов, ограниченных X::Fooable, Y::Fooable и Z::Fooable соответственно, всегда будет выбирать кандидата, ограниченного Z::Fooable. Это почти наверняка не то, что хочет программист.


Стандартные ссылки

Правило подчинения находится в [temp.constr.order]/1.2:

атомное ограничение A вводит другое атомное ограничение B тогда и только тогда, когда A и B идентичны с использованием правил, описанных в [temp.constr.atomic].

Атомные ограничения определены в [temp.constr.atomic]:

Атомное ограничение формируется из выражения E и отображения из параметров шаблона, которые появляются внутри E в аргументах шаблона, включающих параметры шаблона ограниченного объекта, называемого сопоставлением параметров ([temp.constr.decl]). [Примечание. Атомные ограничения формируются путем нормализации ограничения. E никогда не является логическим выражением AND и логическим выражением OR. - конечная нота]

Два атомных ограничения идентичны, если они сформированы из одного и того же выражения, а цели сопоставлений параметров эквивалентны в соответствии с правилами выражений, описанными в [temp.over.link].

Ключевым моментом здесь является то, что атомарные ограничения формируются. Это ключевой момент здесь. В [temp.constr.normal]:

Нормальная форма выражения E является ограничением, которое определяется следующим образом:

  • Нормальная форма выражения (E) является нормальной формой E.
  • Нормальная форма выражения E1 || E2 - дизъюнкция нормальных форм E1 и E2.
  • Нормальная форма выражения E1 && E2 является конъюнкцией нормальных форм E1 и E2.
  • Нормальная форма идентификационного выражения вида С <A 1, А 2, А..., N>, где C называет понятие, является нормальной формой ограничения экспрессии С, после подстановки A 1, A 2 ,..., A n для C соответствующих параметров шаблона в сопоставлениях параметров в каждом атомном ограничении. Если какая-либо такая подстановка приводит к недопустимому типу или выражению, программа плохо сформирована; диагностика не требуется. [...]
  • Нормальная форма любого другого выражения E является атомарным ограничением, выражение которого равно E и отображение параметров которого является тождественным отображением.

Для первой перегрузки foo ограничение C1<T> && C2<T>, поэтому для его нормализации получим конъюнкцию нормальных форм C1<T> 1 и C2<T> 1, а затем мы сделанный. Аналогично, для второй перегрузки foo ограничение является C1<T> 2, которое является его собственной нормальной формой.

Правило для того, что делает атомарные ограничения идентичными, состоит в том, что они должны быть сформированы из одного и того же выражения (конструкция исходного уровня). Хотя обе функции имеют атомное ограничение, которое использует последовательность токенов C1<T>, это не то же самое буквальное выражение в исходном коде.

Следовательно, индексы, указывающие, что это, по сути, не одно и то же атомное ограничение. C1<T> 1 не идентичен C1<T> 2. Правило не является эквивалентом эквивалента! Таким образом, первый foo C1<T> не включает второй foo C1<T> и наоборот.

Следовательно, двусмысленный.

С другой стороны, если бы мы имели:

template <typename T> concept D1 = true;    
template <typename T> concept D2 = true;

template <typename T> requires D1<T> && D2<T> 
constexpr int quux() { return 0; }

template <typename T> requires D1<T> 
constexpr int quux() { return 1; }

Ограничением для первой функции является D1<T> && D2<T>. 3-я пуля дает нам соединение D1<T> и D2<T>. Четвертая пула приводит нас к тому, чтобы мы сами заменили сами понятия, поэтому первая нормализуется в true 1, а вторая - в true 2. Опять же, индексы указывают, к какому true относится.

Ограничением для второй функции является D1<T>, которая нормализует (4-ю пулю) в true 1.

И теперь true 1 действительно является тем же выражением, что и true 1, поэтому эти ограничения считаются идентичными. В результате D1<T> && D2<T> включает D1<T>, а quux<int>() - однозначный вызов, который возвращает 0.