En C++20, la sintaxis para definir funciones genéricas ha sufrido una evolución radical hacia la legibilidad. Lo que antes requería una verbosidad innecesaria con template<typename T> y std::enable_if (SFINAE), ahora se resuelve mediante Abbreviated Function Templates. Esta característica permite usar la palabra clave auto directamente en los parámetros de una función, lo que le indica al compilador que la función es, en realidad, una plantilla.
Cuando combinamos esto con los Concepts [C++20], el código deja de ser solo “genérico” para ser “específicamente restringido”. Un auto por sí solo acepta cualquier tipo, lo cual es peligroso si tu lógica interna asume que el tipo tiene métodos como .begin() o .size(). Con los concepts, podemos escribir auto f(std::integral auto x), indicando que x debe cumplir con el concepto std::integral. El compilador no solo generará el código para el tipo específico, sino que validará que ese tipo sea, efectivamente, un entero antes de intentar compilar el cuerpo de la función.
Esto funciona gracias al mecanismo de Subsumption (subsumción). Cuando el compilador se enfrenta a múltiples sobrecargas que coinciden con un argumento, no elige cualquiera; elige la versión más restringida (la que tenga el concepto más específico). Si defines una función para un std::ranges::input_range y otra para un std::ranges::contiguous_range, y pasas un std::vector, el compilador elegirá la segunda porque es más específica, permitiéndote optimizar el código para tipos que garantizan memoria contigua sin perder la capacidad de aceptar cualquier otro contenedor iterable.
Deberías usar esta sintaxis siempre que estés escribiendo código genérico moderno. Elimina el ruido visual y hace que las intenciones del programador sean evidentes. Sin embargo, si usas auto sin restricciones en funciones que operan sobre propiedades de un tipo (como iterar un contenedor), el error se desplazará al cuerpo de la función, generando mensajes de error de plantilla casi ilegibles.
#include <iostream>
#include <vector>
#include <concepts>
#include <ranges>
#include <string>
// 1. Abbreviated function template básica (catch-all)
void procesar(auto valor) {
std::cout << "Genérico: " << valor << "\n";
}
// 2. Uso de concept en parámetro abreviado (solo tipos integrales)
void procesar(std::integral auto valor) {
std::cout << "Entero (integral): " << valor << "\n";
}
// 3. Uso de conceptos de la STL para rangos
void procesar(std::ranges::input_range auto const& rango) {
std::cout << "Rango (input_range): ";
for (const auto& e : rango) std::cout << e << " ";
std::cout << "\n";
}
// 4. Subsumption: El compilador elegirá esta si el rango es contiguo
void procesar(std::ranges::contiguous_range auto const& rango) {
std::cout << "Rango Contiguo (e.g. std::vector/std::array): ";
// Aquí podríamos usar std::memcpy o punteros si fuera necesario
for (const auto& e : rango) std::cout << e << " ";
std::cout << "\n";
}
int main() {
int n = 42;
double d = 3.14;
std::vector<int> v = {10, 20, 30};
std::string s = "Hola";
// Llama a la versión 2 (std::integral es más específico que auto)
procesar(n);
// Llama a la versión 1 (double no es integral)
procesar(d);
// Llama a la versión 4 (std::vector es contiguous_range, más específico que input_range)
procesar(v);
// Llama a la versión 3 (std::string es un rango, pero no garantiza contigüidad
// de la misma forma que un vector en todos los implementaciones/contextos)
// Nota: En la mayoría de STL modernas, string es contiguous_range,
// pero usemos un caso donde sea solo input_range si fuera el caso.
procesar(s);
return 0;
}
Análisis del ejemplo
Fíjate en cómo el compilador gestiona las llamadas en main(). Cuando llamamos a procesar(n), donde n es un int, el compilador ve dos candidatos válidos: la versión auto (genérica) y la versión std::integral auto. Como std::integral es una restricción más fuerte, se aplica la subsumción y se elige la versión especializada para enteros.
En el caso de procesar(v), donde v es un std::vector<int>, el tipo satisface tres de nuestras funciones: la genérica, la de input_range y la de contiguous_range. El compilador realiza un análisis de la jerarquía de conceptos. Dado que std::ranges::contiguous_range es un subtipo de std::ranges::input_range (requiere todas las propiedades de este último más algunas adicionales), la versión de contiguous_range es la más específica y, por tanto, la seleccionada.
La versión procesar(auto valor) actúa como un “fallback” o captura total. Si pasamos un tipo que no cumple ninguna de las restricciones específicas (como el double), el compilador simplemente recurre a la versión más abierta para evitar un error de compilación.
El error frecuente ocurre cuando intentas definir dos funciones que son igualmente “específicas” pero no tienen una relación de jerarquía clara entre sus conceptos. Por ejemplo:
// Esto causará un error de ambigüedad si intentas pasar algo que cumpla ambos
void error_ambigüedad(std::convertible_to<int> auto x) {}
void error_ambigüedad(std::same_as<int> auto x) {}
Si pasas un int, ambos son candidatos. Aunque same_as es técnicamente más restrictivo, si las reglas de subsumción no logran determinar una jerarquía clara o si los conceptos se solapan de forma no jerárquica, el compilador lanzará un error de ambiguous call. Además, recuerda que usar auto en un parámetro no significa que el tipo se determine en tiempo de ejecución como en Python; es una plantilla que se instanciará en tiempo de compilación. Si intentas usar auto en un contexto que no permite plantillas (como un miembro de clase no estático de una clase no-template), el compilador fallará.
Para verificar la resolución de estas restricciones, puedes usar el flag -Wambiguous-overload en GCC o observar la salida de decltype en un debugger, aunque la mejor herramienta sigue siendo Compiler Explorer para ver cómo se resuelven las llamadas.
N° 57