Interesante pregunta. Recientemente vi la charla de Andrew Sutton sobre conceptos, y en la sesión de preguntas y respuestas alguien hizo la siguiente pregunta (marca de tiempo en el siguiente enlace):
CppCon 2018: Andrew Sutton "Conceptos en 60: todo lo que necesita saber y nada que no"
Entonces la pregunta se reduce a: If I have a concept that says A && B && C, another says C && B && A, would those be equivalent?
Andrew respondió que sí, pero señaló el hecho de que el compilador tiene algunos métodos internos (que son transparentes para el usuario) para descomponer los conceptos en proposiciones lógicas atómicas ( atomic constraints
como Andrew redactó el término) y verificar si son equivalente.
Ahora mira lo que dice cppreference sobre std::same_as
:
std::same_as<T, U>
subsumes std::same_as<U, T>
y viceversa.
Básicamente es una relación "si-y-solo-si": se implican mutuamente. (Equivalencia lógica)
Mi conjetura es que aquí están las restricciones atómicas std::is_same_v<T, U>
. La forma en que los compiladores tratan std::is_same_v
puede hacerlos pensar std::is_same_v<T, U>
y std::is_same_v<U, T>
como dos restricciones diferentes (¡son entidades diferentes!). Entonces, si implementa std::same_as
usando solo uno de ellos:
template< class T, class U >
concept same_as = detail::SameHelper<T, U>;
Entonces std::same_as<T, U>
y std::same_as<U, T>
"explotaría" a diferentes restricciones atómicas y no sería equivalente.
Bueno, ¿por qué le importa al compilador?
Considere este ejemplo :
#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>
subsumes my_same_as<U, T>
; por lo tanto, el compilador debe seleccionar la segunda especialización de plantilla, excepto ... no lo hace: el compilador emite un error error: call of overloaded 'foo(int, int)' is ambiguous
.
La razón detrás de esto es que, dado que my_same_as<U, T>
y my_same_as<T, U>
no subsume entre sí, my_same_as<T, U> && std::integral<T>
y my_same_as<U, T>
se convierten incomparable (en el conjunto parcialmente ordenado de restricciones bajo la relación de subsunción).
Sin embargo, si reemplaza
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>;
El código se compila.
SameHelper<T, U>
podría ser cierto no significa queSameHelper<U, T>
podría serlo.