Глядя на возможную реализацию концепции same_as на https://en.cppreference.com/w/cpp/concepts/same_as, я заметил, что происходит нечто странное.
namespace detail {
template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;
}
template< class T, class U >
concept same_as = detail::SameHelper<T, U> && detail::SameHelper<U, T>;
Первый вопрос: почему SameHelper
понятие вложено? Второе - почему same_as
проверяет, T
совпадает ли U
и U
совпадает с T
? Разве это не избыточно?
SameHelper<T, U>
может быть правдой, не значит,SameHelper<U, T>
может быть.is_same<T, U>::value == true
если и только еслиis_same<U, T>::value == true
». Это подразумевает, что эта двойная проверка не нужнаОтветы:
Интересный вопрос. Недавно я смотрел доклад Эндрю Саттона о концепциях, и в ходе сессии вопросов и ответов кто-то задал следующий вопрос (временная метка в следующей ссылке): CppCon 2018: Эндрю Саттон «Концепции в 60: все, что вам нужно знать, и ничего, чего вы не делаете»
Таким образом, вопрос сводится к следующему:
If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?
Эндрю ответил да, но указал на тот факт, что компилятор имеет некоторые внутренние методы (которые прозрачны для пользователя), чтобы разложить понятия на атомарные логические предложения (atomic constraints
как Эндрю сформулировал термин) и проверить, являются ли они эквивалент.Теперь посмотрим, что говорит cppreference
std::same_as
:Это в основном отношения «если и только если»: они подразумевают друг друга. (Логическая эквивалентность)
Моя гипотеза состоит в том, что здесь атомные ограничения
std::is_same_v<T, U>
. То, как обрабатывают компиляторы,std::is_same_v
может заставить их думатьstd::is_same_v<T, U>
иstd::is_same_v<U, T>
как два разных ограничения (это разные сущности!). Так что если вы реализуете,std::same_as
используя только один из них:Тогда
std::same_as<T, U>
иstd::same_as<U, T>
будет «взорваться» к различным атомным ограничениям и стать не эквивалентны.Ну, а зачем это компилятору?
Рассмотрим этот пример :
В идеале,
my_same_as<T, U> && std::integral<T>
подгруппыmy_same_as<U, T>
; следовательно, компилятор должен выбрать вторую специализацию шаблона, кроме ... это не так: компилятор выдает ошибкуerror: call of overloaded 'foo(int, int)' is ambiguous
.Причиной этого является то, что, так как
my_same_as<U, T>
иmy_same_as<T, U>
не включает друг друга,my_same_as<T, U> && std::integral<T>
иmy_same_as<U, T>
становятся несопоставимыми (на частично упорядоченном множестве ограничений по отношению к подчинению).Однако, если вы замените
с
Код компилируется.
источник
SameHelper
: она делает два использования изis_same_v
проистекают из того же выражения.is_same<T, U>
идентичноis_same<U, T>
, два атомных ограничения не считаются идентичными, если они также не сформированы из одного и того же выражения. Отсюда необходимость в обоих.are_same_as
?template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);
потерпит неудачу в некоторых случаях. Напримерare_same_as<T, U, int>
было бы эквивалентно,are_same_as<T, int, U>
но неare_same_as<U, T, int>
std::is_same
определяется как истина тогда и только тогда, когда:Насколько я знаю, стандарт не определяет значение слова «тот же тип», но в естественном языке и логике «то же самое» является отношением эквивалентности и, следовательно, является коммутативным.
Учитывая это предположение, которое я приписываю,
is_same_v<T, U> && is_same_v<U, V>
действительно будет излишним. Ноsame_as
не указано в терминахis_same_v
; это только для экспозиции.Явная проверка обоих позволяет реализовать для
same-as-impl
удовлетворения,same_as
не будучи коммутативным. Указание этого способа точно описывает поведение концепции без ограничения ее реализации.Почему именно этот подход был выбран вместо определения с точки зрения
is_same_v
, я не знаю. Преимущество выбранного подхода состоит в том, что оба определения не связаны между собой. Одно не зависит от другого.источник