Pergunta interessante. Recentemente, assisti à palestra de Andrew Sutton sobre Conceitos e, na sessão de perguntas e respostas, alguém fez a seguinte pergunta (carimbo de data e hora no link a seguir):
CppCon 2018: Andrew Sutton “Conceitos em 60: tudo o que você precisa saber e nada que não saiba”
Portanto, a pergunta se resume a: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?
Andrew respondeu que sim, mas apontou o fato de que o compilador possui alguns métodos internos (que são transparentes para o usuário) para decompor os conceitos em proposições lógicas atômicas ( atomic constraints
como Andrew formulou o termo) e verificar se elas são equivalente.
Agora veja o que a cppreference diz sobre std::same_as
:
std::same_as<T, U>
subsume std::same_as<U, T>
e vice-versa.
É basicamente um relacionamento "se-e-só-se": eles implicam um ao outro. (Equivalência Lógica)
Minha conjectura é que aqui estão as restrições atômicas std::is_same_v<T, U>
. A maneira como os compiladores tratam std::is_same_v
pode fazê-los pensar std::is_same_v<T, U>
e std::is_same_v<U, T>
como duas restrições diferentes (são entidades diferentes!). Portanto, se você implementar std::same_as
usando apenas um deles:
template< class T, class U >
concept same_as = detail::SameHelper<T, U>;
Então std::same_as<T, U>
e std::same_as<U, T>
"explodiria" para diferentes restrições atômicas e se tornaria não equivalente.
Bem, por que o compilador se importa?
Considere este exemplo :
#include <type_traits>
#include <iostream>
#include <concepts>
template< class T, class U >
concept SameHelper = std::is_same_v<T, U>;
template< class T, class U >
concept my_same_as = SameHelper<T, U>;
// template< class T, class U >
// concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;
template< class T, class U> requires my_same_as<U, T>
void foo(T a, U b) {
std::cout << "Not integral" << std::endl;
}
template< class T, class U> requires (my_same_as<T, U> && std::integral<T>)
void foo(T a, U b) {
std::cout << "Integral" << std::endl;
}
int main() {
foo(1, 2);
return 0;
}
Idealmente, my_same_as<T, U> && std::integral<T>
subsume my_same_as<U, T>
; portanto, o compilador deve selecionar a segunda especialização de modelo, exceto ... isso não acontece: o compilador emite um erro error: call of overloaded 'foo(int, int)' is ambiguous
.
A razão por trás disso é que, desde my_same_as<U, T>
e my_same_as<T, U>
não se subsumem my_same_as<T, U> && std::integral<T>
e my_same_as<U, T>
se tornam incomparáveis (no conjunto de restrições parcialmente ordenadas sob a relação de subsunção).
No entanto, se você substituir
template< class T, class U >
concept my_same_as = SameHelper<T, U>;
com
template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;
O código é compilado.
SameHelper<T, U>
pode ser verdade não significa queSameHelper<U, T>
pode ser.