Pourquoi le concept same_as vérifie-t-il deux fois l'égalité de type?

19

En regardant l'implémentation possible du concept same_as sur https://en.cppreference.com/w/cpp/concepts/same_as, j'ai remarqué qu'il se passe quelque chose d'étrange.

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>;

La première question est pourquoi un SameHelperconcept est intégré? La seconde est pourquoi same_asvérifie si Test le même que Uet Ule même que T? N'est-ce pas redondant?

user7769147
la source
Ce SameHelper<T, U>n'est pas parce que cela pourrait être vrai que cela SameHelper<U, T>pourrait l'être.
Un programmeur mec
1
c'est le point, si a est égal à b, b est égal à un n'est-ce pas?
user7769147
@ user7769147 Oui, et cela définit cette relation.
François Andrieux
4
Hmm la documentation de std :: is_same dit même "La commutativité est satisfaite, c'est-à-dire pour deux types T et U, is_same<T, U>::value == truesi et seulement si is_same<U, T>::value == true." Cela implique que cette double vérification n'est pas nécessaire
Kevin
1
Non, c'est faux, le std :: is_same dit: si et seulement si la condition est vraie, deux types sont commutatifs. Ce n'est pas nécessairement le cas. Mais je ne trouve pas l'exemple de deux types non commutatifs.
Nemanja Boric

Réponses:

16

Question interessante. J'ai récemment regardé la conférence d'Andrew Sutton sur les concepts, et lors de la session de questions / réponses, quelqu'un a posé la question suivante (horodatage dans le lien suivant): CppCon 2018: Andrew Sutton «Concepts in 60: Tout ce que vous devez savoir et rien que vous ne savez pas»

Donc, la question se résume à: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?Andrew a répondu oui, mais a souligné le fait que le compilateur a des méthodes internes (qui sont transparentes pour l'utilisateur) pour décomposer les concepts en propositions logiques atomiques ( atomic constraintscomme Andrew a formulé le terme) et vérifier si elles sont équivalent.

Maintenant, regardez ce que cppreference dit à propos de std::same_as:

std::same_as<T, U>subsume std::same_as<U, T>et vice versa.

Il s'agit essentiellement d'une relation «si et seulement si»: elles s'impliquent mutuellement. (Équivalence logique)

Ma conjecture est qu'ici les contraintes atomiques sont std::is_same_v<T, U>. La façon dont les compilateurs traitent std::is_same_vpeut les faire réfléchir std::is_same_v<T, U>et std::is_same_v<U, T>comme deux contraintes différentes (ce sont des entités différentes!). Donc, si vous implémentez std::same_asun seul d'entre eux:

template< class T, class U >
concept same_as = detail::SameHelper<T, U>;

Alors std::same_as<T, U>et std::same_as<U, T>"exploserait" à différentes contraintes atomiques et deviendrait pas équivalent.

Eh bien, pourquoi le compilateur s'en soucie-t-il?

Considérez cet exemple :

#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;
}

Idéalement, my_same_as<T, U> && std::integral<T>subsume my_same_as<U, T>; par conséquent, le compilateur doit sélectionner la deuxième spécialisation de modèle, sauf ... ce n'est pas le cas: le compilateur émet une erreur error: call of overloaded 'foo(int, int)' is ambiguous.

La raison en est que depuis my_same_as<U, T>et my_same_as<T, U>ne se subsument pas, my_same_as<T, U> && std::integral<T>et my_same_as<U, T>deviennent incomparables (sur l'ensemble de contraintes partiellement ordonnées sous la relation de subsomption).

Cependant, si vous remplacez

template< class T, class U >
concept my_same_as = SameHelper<T, U>;

avec

template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;

Le code se compile.

Rin Kaenbyou
la source
same_as <T, U> et same_as <U, T> pourraient également être des contraintes atomiques différentes mais leur résultat serait toujours le même. Pourquoi le compilateur se soucie tant de définir same_as que deux contraintes atomiques différentes qui d'un point de vue logique sont les mêmes?
user7769147
2
Le compilateur est nécessaire de considérer les deux expressions comme distincte pour subsomption contrainte, mais il peut envisager des arguments à eux de la façon évidente. Donc, non seulement nous avons besoin des deux directions (de sorte que peu importe l'ordre dans lequel elles sont nommées lors de la comparaison des contraintes), nous avons également besoin SameHelper: cela fait que les deux utilisations de is_same_vdérivent de la même expression.
Davis Herring
@ user7769147 Voir la réponse mise à jour.
Rin Kaenbyou
1
Il semble que la sagesse conventionnelle soit erronée en ce qui concerne l'égalité des concepts. Contrairement aux modèles où is_same<T, U>est identique à is_same<U, T>, deux contraintes atomiques ne sont considérées comme identiques que si elles sont également formées à partir de la même expression. D'où la nécessité des deux.
AndyG
Et alors are_same_as? template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);échouerait dans certains cas. Par exemple are_same_as<T, U, int>serait équivalent are_same_as<T, int, U>mais pas àare_same_as<U, T, int>
user7769147
2

std::is_same est défini comme vrai si et seulement si:

T et U nomment le même type avec les mêmes qualifications cv

Pour autant que je sache, la norme ne définit pas le sens de «même type», mais en langage naturel et en logique, «même» est une relation d'équivalence et est donc commutatif.

Étant donné cette hypothèse, à laquelle je souscris, is_same_v<T, U> && is_same_v<U, V>serait en effet redondante. Mais same_­asn'est pas spécifié en termes de is_same_v; ce n'est que pour l'exposition.

La vérification explicite des deux permet à l'implémentation same-as-implde satisfaire same_­assans être commutative. Le spécifier de cette façon décrit exactement comment le concept se comporte sans restreindre comment il pourrait être mis en œuvre.

Exactement pourquoi cette approche a été choisie au lieu de préciser en termes de is_same_v, je ne sais pas. Un avantage de l'approche choisie est sans doute que les deux définitions sont découplées. L'un ne dépend pas de l'autre.

eerorika
la source
2
Je suis d'accord avec vous, mais ce dernier argument est un peu exagéré. Pour moi, cela ressemble à: "Hé, j'ai ce composant réutilisable qui me dit si deux types sont les mêmes. Maintenant, j'ai cet autre composant qui doit savoir si les types sont les mêmes, mais, au lieu de réutiliser mon composant précédent , Je vais juste créer une solution ad hoc spécifique à ce cas. Maintenant, j'ai «découplé» le gars qui a besoin de la définition de l'égalité de celui qui a la définition de l'égalité. Yay! "
Cássio Renan
1
@ CássioRenan Bien sûr. Comme je l'ai dit, je ne sais pas pourquoi, c'est juste le meilleur raisonnement que je pourrais trouver. Les auteurs peuvent avoir une meilleure justification.
eerorika