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 passait 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 concept SameHelper
est intégré? La seconde est pourquoi same_as
vérifie si T
est identique à U
et U
identique à T
? N'est-ce pas redondant?
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 pas "
La question se résume donc à: 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 avait des méthodes internes (qui sont transparentes pour l'utilisateur) pour décomposer les concepts en propositions logiques atomiques (atomic constraints
comme Andrew a formulé le terme) et vérifiez si elles sont équivalentes.
Maintenant, regardez ce que cppreference dit à propos de std::same_as
:
std::same_as<T, U>
Subsumestd::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_v
Pourrait leur faire penser 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_as
En utilisant un 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>
"Exploseraient" en différentes contraintes atomiques et ne deviendraient pas équivalents.
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 derrière cela est que puisque 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 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.
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.
Compte tenu de cette hypothèse, que j'attribue, is_same_v<T, U> && is_same_v<U, V>
serait en effet redondant. Mais same_as
n'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 de same-as-impl
satisfaire same_as
sans être commutatif. 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 spécifier 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.