¿Por qué el mismo concepto verifica la igualdad de tipos dos veces?

19

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 SameHelperconcepto está integrado? La segunda es ¿por qué same_ascomprueba si Tes igual Uo Uigual que T? ¿No es redundante?

usuario7769147
fuente
El hecho de que SameHelper<T, U>podría ser cierto no significa que SameHelper<U, T>podría serlo.
Algún tipo programador el
1
ese es el punto, si a es igual a b, b es igual a a ¿no?
user7769147
@ user7769147 Sí, y esto define esa relación.
François Andrieux
44
Hmm, la documentación de std :: is_same incluso dice "La conmutatividad se cumple, es decir, para cualquiera de los dos tipos T y U, is_same<T, U>::value == truesi y solo si is_same<U, T>::value == true". Esto implica que esta doble verificación no es necesaria
Kevin
1
No, esto está mal, dice std :: is_same: si y solo si la condición se cumple, dos tipos son conmutativos. Esto no es necesariamente así. Pero no encuentro el ejemplo de dos tipos no conmutativos.
Nemanja Boric

Respuestas:

16

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 constraintscomo 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_vpuede 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_asusando 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.

Rin Kaenbyou
fuente
same_as <T, U> y same_as <U, T> también podrían ser diferentes contracciones atómicas, pero su resultado sería el mismo. ¿Por qué al compilador le importa tanto definir same_as como dos restricciones atómicas diferentes que desde un punto de vista lógico son las mismas?
user7769147
2
El compilador es necesario tener en cuenta las dos expresiones como distinta de la subsunción limitación, pero se puede considerar argumentos a ellos de la manera obvia. Entonces, no solo necesitamos ambas direcciones (de modo que no importa en qué orden se llamen cuando se comparan restricciones), también necesitamos SameHelper: hace que los dos usos de is_same_vderiven de la misma expresión.
Davis Herring
@ user7769147 Ver respuesta actualizada.
Rin Kaenbyou, el
1
Parece que la sabiduría convencional está mal con respecto al concepto de igualdad. A diferencia de las plantillas donde is_same<T, U>es idéntico is_same<U, T>, dos restricciones atómicas no se consideran idénticas a menos que también se formen a partir de la misma expresión. De ahí la necesidad de ambos.
AndyG
¿Qué hay de are_same_as? template<typename T, typename U0, typename... Un> concept are_same_as = SameAs<T, U0> && (SameAs<T, Un> && ...);fallaría en algunos casos. Por ejemplo are_same_as<T, U, int>, sería equivalente are_same_as<T, int, U>pero no aare_same_as<U, T, int>
user7769147
2

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 atribuyo, is_same_v<T, U> && is_same_v<U, V>sería redundante. Pero same_­asno 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 para same-as-implsatisfacer same_­assin ser conmutativo. Al especificarlo de esta manera se describe exactamente cómo se comporta el concepto sin restringir cómo se podría implementar.

is_same_vNo sé exactamente por qué se eligió este enfoque en lugar de especificarlo . Una ventaja del enfoque elegido es posiblemente que las dos definiciones están desacopladas. Uno no depende del otro.

eerorika
fuente
2
Estoy de acuerdo con usted, pero este último argumento es un poco exagerado. Para mí, suena como: "Oye, tengo este componente reutilizable que me dice si dos tipos son iguales. Ahora tengo este otro componente que necesita saber si los tipos son iguales, pero, en lugar de reutilizar mi componente anterior , Crearé una solución ad-hoc específica para este caso. Ahora he 'desacoplado' al tipo que necesita la definición de igualdad del tipo que tiene la definición de igualdad. ¡Yay! "
Cássio Renan
1
@ CássioRenan Claro. Como dije, no sé por qué, ese es el mejor razonamiento que se me ocurre. Los autores pueden tener una mejor justificación.
eerorika