Reglas de Inferencia y Deducción de Tipos en C++

Reglas de Inferencia y Deducción de Tipos en C++

Inferencia de Tipos

La palabra clave auto

auto solo puede utilizarse en inicializaciones, sirve para inferir tipos automáticamente y funciona en tiempo de compilación.

Es importante destacar que auto solo infiere el tipo del valor, eliminando修饰adores como const y referencias. Por supuesto, estos modificadores pueden combinarse con auto.

A partir de C++14, auto puede utilizarse en parámetros de funciones lambda, y desde C++20 esta característica se extendió a funciones normales y valores de retorno de plantillas. Consideremos el siguiente ejemplo:

auto sumar14 = [](auto x, auto y) -> int {
    return x + y;
}

auto sumar20(auto x, auto y) {
    return x + y;
//El compilador genera automáticamente un conjunto de plantillas.
int a = 5;  // tipo int
int b = 6;  // tipo int
std::cout << sumar14(a, b) << std::endl;
std::cout << sumar20(a, b) << std::endl;

[!WARNING]

auto todavía no puede utilizarse para deducir tipos de arrays:

auto arr_auto[10] = {arr};  // Error, no se puede deducir el tipo de elementos del array

2.6.auto.cpp:30:19: error: 'arr_auto' declarado como array de 'auto'
auto arr_auto[10] = {arr};

Tampoco se permite usarlo directamente en la generación de plantillas, como por ejemplo vector<auto>.

La palabra clave decltype

La palabra clave decltype apareció para solucionar las limitaciones de auto, que solo puede deducir tipos de variables. decltype se utiliza para manejar expresiones.

Por lo tanto, decltype puede deducir categorías de valor como const y referencias, preservando estrictamente la categoría de la expresión en lugar de solo el tipo.

Un uso común es el siguiente:

int valor1 = 10;
double valor2 = 100.1;
decltype(valor1 + valor2) resultado = valor1 + valor2;
//decltype puede deducir el tipo resultante de la expresión, aunque aquí también podría usarse auto.

C++11 también introdujo lo que se conoce como tipo de retorno trailing (tipo de retorno al final), utilizando la palabra clave auto para postergar el tipo de retorno e inferir el tipo de retorno de la plantilla:

template
auto sumar2(T x, U y) -> decltype(x + y) {
return x + y;
}

A partir de C++14, los funciones normales pueden tener deducción de retorno, por lo que la siguiente写法变得合法:

template
auto sumar3(T x, U y) {
return x + y;
}

La expresión decltype(auto)

decltype(auto) analiza el tipo y la categoría de valor de la expresión como lo hace decltype, pero sintácticamente no requiere especificar la expresión explícitamente como auto.

Reglas de Deducción de Tipos

Las reglas de deducción de tipos son bastante intuitivas, lo cual es precisamente su objetivo, así que no es necesario memorizarlas, solo comprenderlas.

Deducción de Tipos en Plantillas

La deducción de tipos de auto se basa en plantillas, así que veamos las reglas de las plantillas.

Supongamos un caso básico:

template
f(ParamType param);

f(expresion);

La deducción de ParamType y T es diferente, simplemente porque ParamType puede llevar varios modificadores como punteros o const.

Aquí hay tres casos:

  • ParamType es un puntero o referencia, pero no una referencia universal
  • ParamType es una referencia universal
  • ParamType no es ni puntero ni referencia

Primer Caso

Si expr es una referencia, se ignora la parte de referencia, y luego se hace pattern matching con ParamType para determinar T.

Por ejemplo, si esta es nuestra plantilla,

template
void f(T& param);  //param es una referencia

Declaramos estas variables,

int x = 27;           //x es int
const int cx = x;     //cx es const int
const int& rx = x;    //rx es una referencia a x como const int

En diferentes llamadas, los tipos deducidos para param y T serían:

f(x);     //T es int, el tipo de param es int&
f(cx);    //T es const int, el tipo de param es const int&
f(rx);    //T es const int, el tipo de param es const int&

Segundo Caso

  • Si expr es un valor izquierdo, tanto T como ParamType se deducen como referencias de valor izquierdo. Esto es muy inusual: primero, es el único caso en la deducción de tipos de plantillas donde T se deduce como referencia. Segundo, aunque ParamType se declare con tipo de referencia de valor derecho, el resultado final es una referencia de valor izquierdo.
  • Si expr es un valor derecho, se usan las reglas de deducción normales (el caso uno).

Por ejemplo:

template
void f(T&& param);  //param ahora es un tipo de referencia universal, && con deduccción es referencia universal.

int x = 27;               //como antes
const int cx = x;         //como antes
const int& rx = cx;       //como antes

f(x);     //x es valor izquierdo, así que T es int&,
//el tipo de param también es int&

f(cx);    //cx es valor izquierdo, así que T es const int&,
//el tipo de param también es const int&

f(rx);    //rx es valor izquierdo, así que T es const int&,
//el tipo de param también es const int&

f(27);    //27 es valor derecho, así que T es int,
//el tipo de param es int&&

El caso de referencia universal es: si se pasa un valor izquierdo, es una referencia de valor izquierdo; si se pasa un valor derecho, es una referencia de valor derecho.

De hecho, aquí T tiene &, que puede sustituirse directamente en ParamType, eliminando duplicados (reglas de折叠/de colapso de referencias) y se descubre que es simplemente ParamType.

Tercer Caso

Esto significa que sin importar qué se pase, param se convierte en una copia: un objeto completamente nuevo. De hecho, este comportamiento de que param se convierta en un nuevo objeto afecta cómo T se deduce a partir de expr.

  • Como antes, si el tipo de expr es una referencia, se ignora esta parte de referencia
  • Si después de ignorar la referencia de expr, expr es const, entonces se ignora también el const. Si es volatile, también se ignora el volatile
template
void f(T param);  //param se maneja por valor

int x = 27;           //como antes
const int cx = x;     //como antes
const int& rx = cx;   //como antes

f(x);     //T y param son ambos int
f(cx);    //T y param son ambos int
f(rx);    //T y param son ambos int

Porque esto es una copia.

Casos Especiales

Si el array se pasa usando el primer caso, se preserva el tipo de array, no hay degradación. El grado de preservación nos permite escribir esto:

//En tiempo de compilación devuelve un valor constante del tamaño del array
//(el parámetro array no tiene nombre porque solo nos importa el tamaño)
template
constexpr std::size_t tamanoArray(T (&)[N]) noexcept
{
return N;
}

Aunque no tiene mucha utilidad.

Deducción de Tipos con auto

Es similar a las plantillas: la deducción de tipos con auto tiene una correspondencia directa con la deducción de tipos de plantillas. Existe una transformación muy sistemática y regular entre ellas.

template
void f(ParamType param);
f(expresion);  //llamar f usando alguna expresión

//Cuando una varible se declara con auto, auto toma el papel de T en la plantilla,
//y el especificador de tipo de la varible toma el papel de ParamType.

Solo hay una excepción: las listas de inicialización.

Cómo verificar la deducción de tipos

Usa tu LSP, amigo mío.

Publicado el 7-4 02:00