Conceptos de C ++ 20: ¿Qué especialización de plantilla se elige cuando el argumento de plantilla califica para múltiples conceptos?

23

Dado:

#include <concepts>
#include <iostream>

template<class T>
struct wrapper;

template<std::signed_integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "signed_integral" << std::endl;
    }
};

template<std::integral T>
struct wrapper<T>
{
    wrapper() = default;
    void print()
    {
        std::cout << "integral" << std::endl;
    }
};

int main()
{
    wrapper<int> w;
    w.print(); // Output : signed_integral
    return 0;
}

Del código anterior, intcalifica a ambos std::integraly al std::signed_integralconcepto.

Sorprendentemente, esto compila e imprime "igned_integral "en los compiladores GCC y MSVC. Esperaba que fallara con un error en la línea de "especialización de plantilla ya definida".

Bien, eso es legal, bastante justo, pero ¿por qué fue std::signed_integralelegido en lugar de std::integral? ¿Hay alguna regla definida en el estándar con qué especialización de plantilla se elige cuando varios conceptos califican para el argumento de plantilla?

Lewis Liman
fuente
No diría que es legal solo por el hecho de que los compiladores lo aceptan, especialmente en las primeras etapas de su adopción.
Slava
@Slava en este caso, los conceptos están cuidadosamente diseñados para que se incluyan entre sí de manera intuitiva
Guillaume Racicot
@GuillaumeRacicot está bien, acabo de comentar que la conclusión "es legal porque el compilador lo aceptó" es, digamos, engañoso. Sin embargo, no dije que esto no es legal.
Slava

Respuestas:

14

Esto se debe a que los conceptos pueden ser más especializados que otros, como el orden de las plantillas. Esto se llama ordenación parcial de restricciones.

En el caso de los conceptos, se subsumen entre sí cuando incluyen restricciones equivalentes. Por ejemplo, aquí se explica cómo std::integraly cómo std::signed_integralse implementan:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T> //   v--------------v---- Using the contraint defined above
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Normalizando las restricciones, el compilador reduce la expresión de restricción a esto:

template<typename T>
concept integral = std::is_integral_v<T>;

template<typename T>
concept signed_integral = std::is_integral_v<T> && std::is_signed_v<T>;

En este ejemplo, signed_integralimplica integralcompletamente. Entonces, en cierto sentido, una integral con signo está "más restringida" que una integral.

El estándar lo escribe así:

De [temp.func.order] / 2 (el énfasis es mío):

La ordenación parcial selecciona cuál de las dos plantillas de función es más especializada que la otra mediante la transformación de cada plantilla a su vez (ver el siguiente párrafo) y realizar la deducción de argumentos de plantilla utilizando el tipo de función. El proceso de deducción determina si una de las plantillas es más especializada que la otra. Si es así, la plantilla más especializada es la elegida por el proceso de pedido parcial. Si ambas deducciones tienen éxito, el orden parcial selecciona la plantilla más restringida como se describe en las reglas en [temp.constr.order] .

Eso significa que si hay múltiples sustituciones posibles para una plantilla y ambas se eligen del orden parcial, seleccionará la plantilla más restringida.

Desde [temp.constr.order] / 1 :

Una restricción P subsume una restricción Q si y solo si, para cada cláusula disyuntiva P i en la forma normal disyuntiva de P , P i subsume cada cláusula conjuntiva Q j en la forma normal conjuntiva de Q , donde

  • una cláusula disyuntiva P i subsume una cláusula conjuntiva Q j si y solo si existe una restricción atómica P ia en P i para la cual existe una restricción atómica Q jb en Q j tal que P ia subsume Q jb , y

  • una restricción atómica A subsume otra restricción atómica B si y solo si A y B son idénticos usando las reglas descritas en [temp.constr.atomic] .

Esto describe el algoritmo de subsunción que el compilador usa para ordenar las restricciones y, por lo tanto, los conceptos.

Racicot Guillaume
fuente
2
Parece que te estás apagando en medio de un párrafo ...
ShadowRanger
11

C ++ 20 tiene un mecanismo para decidir cuándo una entidad restringida particular está "más restringida" que otra. Esto no es una cosa simple.

Esto comienza con el concepto de dividir una restricción en sus componentes atómicos, un proceso llamado normalización de restricción . Es grande y demasiado complejo entrar aquí, pero la idea básica es que cada expresión en una restricción se desglosa en sus piezas conceptuales atómicas, recursivamente, hasta llegar a una sub-expresión componente que no es un concepto.

Por lo tanto, veamos cómo se definen los conceptos integraly :signed_integral

template<class T>
  concept integral = is_integral_v<T>;
template<class T>
  concept signed_­integral = integral<T> && is_signed_v<T>;

La descomposición de integrales justa is_integral_v. La descomposición de signed_integrales is_integral_v && is_signed_v.

Ahora, llegamos al concepto de subsunción de restricción . Es un poco complicado, pero la idea básica es que se dice que una restricción C1 "subsume" una restricción C2 si la descomposición de C1 contiene todas las subexpresiones en C2. Podemos ver que integralno subsume signed_integral, pero signed_integral lo hace subsume integral, ya que contiene todo lo que integralhace.

A continuación, llegamos a ordenar entidades restringidas:

Una declaración D1 está al menos tan restringida como una declaración D2 si * D1 y D2 son declaraciones restringidas y las restricciones asociadas a D1 subsumen las de D2; o * D2 no tiene restricciones asociadas.

Como signed_integralsubsumes integral, el <signed_integral> wrapperes "al menos tan restringido" como el <integral> wrapper. Sin embargo, lo contrario no es cierto, debido a que la subsunción no es reversible.

Por lo tanto, de acuerdo con la regla para entidades "más restringidas":

Una declaración D1 está más restringida que otra declaración D2 cuando D1 está al menos tan restringido como D2, y D2 no está al menos tan restringido como D1.

Como <integral> wrapperno está al menos tan restringido como <signed_integral> wrapper, este último se considera más restringido que el primero.

Y por lo tanto, cuando los dos podrían postularse, gana la declaración más restringida.


Tenga en cuenta que las reglas de subsunción de restricción se detienen cuando se encuentra una expresión que no es a concept. Entonces, si hiciste esto:

template<typename T>
constexpr bool my_is_integral_v = std::is_integral_v<T>;

template<typename T>
concept my_signed_integral = my_is_integral_v<T> && std::is_signed_v<T>;

En este caso, my_signed_integral no subsumiría std::integral. Aunque my_is_integral_vse define de manera idéntica std::is_integral_v, porque no es un concepto, las reglas de subsunción de C ++ no pueden analizarlo para determinar si son lo mismo.

Por lo tanto, las reglas de subsunción lo alientan a construir conceptos a partir de operaciones sobre conceptos atómicos.

Nicol Bolas
fuente
3

Con Parcial_ordering_of_constraints

Se dice que una restricción P subsume la restricción Q si se puede demostrar que P implica Q hasta la identidad de restricciones atómicas en P y Q.

y

La relación de subsunción define el orden parcial de las restricciones, que se utiliza para determinar:

  • El mejor candidato viable para una función sin plantilla en resolución de sobrecarga
  • la dirección de una función que no es de plantilla en un conjunto de sobrecarga
  • la mejor coincidencia para un argumento de plantilla de plantilla
  • ordenamiento parcial de especializaciones de plantilla de clase
  • ordenamiento parcial de plantillas de funciones

Y el concepto std::signed_integralsubsume el std::integral<T>concepto:

template < class T >
concept signed_integral = std::integral<T> && std::is_signed_v<T>;

Entonces su código está bien, ya que std::signed_integrales más "especializado".

Jarod42
fuente