Observando a possível implementação do conceito same_as em https://en.cppreference.com/w/cpp/concepts/same_as, notei que algo estranho está acontecendo.
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>;
A primeira pergunta é por que um SameHelper
conceito está incorporado? A segunda é por que same_as
verifica se T
é igual U
e U
igual a T
? Não é redundante?
SameHelper<T, U>
pode ser verdade não significa queSameHelper<U, T>
pode ser.is_same<T, U>::value == true
se e somente seis_same<U, T>::value == true
". Isto implica que esta dupla verificação não é necessáriaRespostas:
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
:É 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 tratamstd::is_same_v
pode fazê-los pensarstd::is_same_v<T, U>
estd::is_same_v<U, T>
como duas restrições diferentes (são entidades diferentes!). Portanto, se você implementarstd::same_as
usando apenas um deles:Então
std::same_as<T, U>
estd::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 :
Idealmente,
my_same_as<T, U> && std::integral<T>
subsumemy_same_as<U, T>
; portanto, o compilador deve selecionar a segunda especialização de modelo, exceto ... isso não acontece: o compilador emite um erroerror: call of overloaded 'foo(int, int)' is ambiguous
.A razão por trás disso é que, desde
my_same_as<U, T>
emy_same_as<T, U>
não se subsumemmy_same_as<T, U> && std::integral<T>
emy_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
com
O código é compilado.
fonte
SameHelper
: faz os dois usos deis_same_v
derivar da mesma expressão.is_same<T, U>
é idênticois_same<U, T>
, duas restrições atômicas não são consideradas idênticas, a menos que também sejam formadas a partir da mesma expressão. Daí a necessidade de ambos.are_same_as
?template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);
falharia em alguns casos. Por exemplo,are_same_as<T, U, int>
seria equivalente a,are_same_as<T, int, U>
mas não aare_same_as<U, T, int>
std::is_same
é definido como verdadeiro se e somente se:Até onde eu sei, padrão não define o significado de "mesmo tipo", mas na linguagem natural e na lógica "igual" é uma relação de equivalência e, portanto, é comutativa.
Dada essa suposição, à qual atribuo,
is_same_v<T, U> && is_same_v<U, V>
seria de fato redundante. Massame_as
não é especificado em termos deis_same_v
; isso é apenas para exposição.A verificação explícita de ambos permite que a implementação
same-as-impl
seja satisfeitasame_as
sem ser comutativa. A especificação desta maneira descreve exatamente como o conceito se comporta sem restringir como ele poderia ser implementado.Exatamente por que essa abordagem foi escolhida, em vez de especificar em termos de
is_same_v
, eu não sei. Uma vantagem da abordagem escolhida é, sem dúvida, que as duas definições são dissociadas. Um não depende do outro.fonte