c++ - ¿Por qué el mismo concepto verifica la igualdad de tipos dos veces?
c++20 (2)
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
atomic constraints
(
atomic constraints
como Andrew redactó el término) y verificar si son equivalentes.
Ahora mire lo que dice cppreference sobre
std::same_as
:
std::same_as<T, U>
subsumestd::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í las restricciones atómicas son
std::is_same_v<T, U>
.
La forma en que los compiladores tratan
std::is_same_v
podría hacerles 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ían" a diferentes restricciones atómicas y no serían equivalentes.
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>
subsume
my_same_as<U, T>
;
por lo tanto, el compilador debe seleccionar la segunda especialización de plantilla, excepto que ... no lo hace: el compilador emite un error de
error: call of overloaded ''foo(int, int)'' is ambiguous
.
La razón detrás de esto es que ya que
my_same_as<U, T>
y
my_same_as<T, U>
no se subsumen entre sí,
my_same_as<T, U> && std::integral<T>
y
my_same_as<U, T>
vuelven incomparables ( en el conjunto de restricciones parcialmente ordenadas 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.
Al observar la posible implementación del concepto same_as en https://en.cppreference.com/w/cpp/concepts/same_as noté que algo extraño está sucediendo.
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 primera pregunta es ¿por qué un concepto SameHelper está incorporado?
El segundo es por qué
same_as
comprueba si
T
es igual a
U
y
U
igual que
T
?
¿No es redundante?
std::is_same
se define como verdadero si y solo si:
T y U nombran el mismo tipo con las mismas calificaciones de CV
Hasta donde sé, estándar no define el significado de "mismo tipo", pero en lenguaje natural y lógica "mismo" es una relación de equivalencia y, por lo tanto, es conmutativa.
Dado este supuesto, al que
is_same_v<T, U> && is_same_v<U, V>
,
is_same_v<T, U> && is_same_v<U, V>
sería de hecho redundante.
Pero
same_as
no se especifica en términos de
is_same_v
;
eso es solo para exposición.
La verificación explícita de ambos permite la implementación de
same-as-impl
para satisfacer
same_as
sin ser conmutativo.
Al especificarlo de esta manera se describe exactamente cómo se comporta el concepto sin restringir cómo se podría implementar.
No sé exactamente por qué se eligió este enfoque en lugar de especificarlo en términos de
is_same_v
.
Una ventaja del enfoque elegido es posiblemente que las dos definiciones están desacopladas.
Uno no depende del otro.