Por que o conceito same_as verifica a igualdade de tipos duas vezes?

19

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 SameHelperconceito está incorporado? A segunda é por que same_asverifica se Té igual Ue Uigual a T? Não é redundante?

user7769147
fonte
Só porque SameHelper<T, U>pode ser verdade não significa que SameHelper<U, T>pode ser.
Algum programador,
11
esse é o ponto, se a é igual a b, b é igual a não é?
user7769147
@ user7769147 Sim, e isso está definindo essa relação.
François Andrieux
4
Hmm, a documentação para std :: is_same diz até "A comutatividade é satisfeita, ou seja, para dois tipos T e U, is_same<T, U>::value == truese e somente se is_same<U, T>::value == true". Isto implica que esta dupla verificação não é necessária
Kevin
11
Não, isso está errado, diz std :: is_same: se e somente se a condição for válida, dois tipos serão comutativos. Isto não é necessariamente verdade. Mas não consigo encontrar o exemplo de dois tipos não comutativos.
Nemanja Boric

Respostas:

16

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 constraintscomo 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_vpode 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_asusando 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.

Rin Kaenbyou
fonte
same_as <T, U> e same_as <U, T> também poderiam ser diferentes restrições atômicas, mas o resultado ainda seria o mesmo. Por que o compilador se preocupa tanto em definir same_as quanto em duas restrições atômicas diferentes que, do ponto de vista lógico, são iguais?
user7769147
2
O compilador é necessário considerar quaisquer duas expressões como distinta para subsunção restrição, mas pode considerar argumentos a eles da maneira óbvia. Assim, não só precisamos ambos os sentidos (de modo que não importa em que ordem eles estão nomeados ao comparar restrições), também precisamos SameHelper: faz os dois usos de is_same_vderivar da mesma expressão.
Davis Herring
@ user7769147 Ver resposta atualizada.
Rin Kaenbyou
11
Parece que a sabedoria convencional está errada em relação à igualdade de conceitos. Ao contrário dos modelos onde is_same<T, U>é idêntico is_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.
AndyG 5/11
Que tal 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>
user7769147 5/11/19
2

std::is_same é definido como verdadeiro se e somente se:

T e U nomeiam o mesmo tipo com as mesmas qualificações cv

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. Mas same_­asnão é especificado em termos de is_same_v; isso é apenas para exposição.

A verificação explícita de ambos permite que a implementação same-as-implseja satisfeita same_­assem 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.

eerorika
fonte
2
Eu concordo com você, mas esse último argumento é um pouco exagerado. Para mim, soa como: "Ei, eu tenho esse componente reutilizável que me diz se dois tipos são iguais. Agora eu tenho esse outro componente que precisa saber se os tipos são iguais, mas, em vez de reutilizar meu componente anterior , Apenas criarei uma solução ad-hoc específica para este caso. Agora 'desacoplei' o cara que precisa da definição de igualdade do cara que tem a definição de igualdade. Sim! "
Cássio Renan
11
@ CássioRenan Claro. Como eu disse, não sei por que, é apenas o melhor raciocínio que eu poderia apresentar. Os autores podem ter uma melhor justificativa.
eerorika