Domanda interessante. Di recente ho visto il discorso di Andrew Sutton su Concepts e nella sessione di domande e risposte qualcuno ha posto la seguente domanda (data e ora nel seguente link):
CppCon 2018: Andrew Sutton "Concepts in 60: Tutto ciò che devi sapere e niente che non sai"
Quindi la domanda si riduce a: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?
Andrew ha risposto di sì, ma ha sottolineato il fatto che il compilatore ha alcuni metodi interni (che sono trasparenti per l'utente) per scomporre i concetti in proposizioni logiche atomiche ( atomic constraints
come Andrew ha formulato il termine) e verificare se sono equivalente.
Ora guarda cosa dice cppreference su std::same_as
:
std::same_as<T, U>
subsume std::same_as<U, T>
e viceversa.
È fondamentalmente una relazione "if-and-only-if": si implicano a vicenda. (Equivalenza logica)
La mia congettura è che qui ci sono i vincoli atomici std::is_same_v<T, U>
. Il modo in cui i compilatori trattano std::is_same_v
potrebbe farli pensare std::is_same_v<T, U>
e std::is_same_v<U, T>
come due vincoli diversi (sono entità diverse!). Quindi, se si implementa std::same_as
utilizzando solo uno di essi:
template< class T, class U >
concept same_as = detail::SameHelper<T, U>;
Poi std::same_as<T, U>
e std::same_as<U, T>
avrebbe "esplodere" a diversi vincoli atomiche e diventare non equivalenti.
Bene, perché al compilatore interessa?
Considera questo esempio :
#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>
sussume my_same_as<U, T>
; pertanto, il compilatore dovrebbe selezionare la seconda specializzazione del modello, tranne ... non lo fa: il compilatore genera un errore error: call of overloaded 'foo(int, int)' is ambiguous
.
La ragione di ciò è che da allora my_same_as<U, T>
e my_same_as<T, U>
non si susseguono a vicenda my_same_as<T, U> && std::integral<T>
e my_same_as<U, T>
diventano incomparabili (sull'insieme parzialmente ordinato di vincoli in relazione alla sussunzione).
Tuttavia, se si sostituisce
template< class T, class U >
concept my_same_as = SameHelper<T, U>;
con
template< class T, class U >
concept my_same_as = SameHelper<T, U> && SameHelper<U, T>;
Il codice viene compilato.
SameHelper<T, U>
potrebbe essere vero, non significa cheSameHelper<U, T>
potrebbe esserlo.