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 constraints
comme 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_v
peut 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_as
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>
"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.
SameHelper<T, U>
n'est pas parce que cela pourrait être vrai que celaSameHelper<U, T>
pourrait l'être.