Las plantillas son una característica del lenguaje de programación C++ que permite que las funciones y clases operen con tipos genéricos . Esto permite que una declaración de función o clase haga referencia a través de una variable genérica a otra clase diferente ( tipo de datos incorporado o recientemente declarado ) sin crear una declaración completa para cada una de estas clases diferentes.
En términos sencillos, una clase o función con plantilla sería el equivalente a (antes de "compilar") copiar y pegar el bloque de código con plantilla donde se utiliza y luego reemplazar el parámetro de plantilla con el real. Por este motivo, las clases que emplean métodos con plantilla colocan la implementación en los encabezados (archivos *.h) ya que no se puede compilar ningún símbolo sin conocer el tipo de antemano.
La biblioteca estándar de C++ proporciona muchas funciones útiles dentro de un marco de plantillas conectadas.
Las principales inspiraciones para las plantillas de C++ fueron los módulos parametrizados proporcionados por el lenguaje CLU y los genéricos proporcionados por Ada . [1]
Existen tres tipos de plantillas: plantillas de función , plantillas de clase y, desde C++14 , plantillas de variable . Desde C++11 , las plantillas pueden ser variádicas o no variádicas; en versiones anteriores de C++ siempre eran no variádicas.
Una plantilla de función se comporta como una función, excepto que la plantilla puede tener argumentos de muchos tipos diferentes (ver ejemplo). En otras palabras, una plantilla de función representa una familia de funciones. El formato para declarar plantillas de función con parámetros de tipo es:
plantilla < identificador de clase > declaración ; plantilla < identificador de nombre de tipo > declaración ;
Ambas expresiones tienen el mismo significado y se comportan exactamente de la misma manera. La última forma se introdujo para evitar confusiones, [2] ya que un parámetro de tipo no necesita ser una clase hasta C++20. (Puede ser un tipo básico como int
o double
.)
Por ejemplo, la biblioteca estándar de C++ contiene la plantilla de función max(x, y)
que devuelve el valor mayor de x
y y
. Esa plantilla de función podría definirse de la siguiente manera:
plantilla < typename T > const T & max ( const T & a , const T & b ) { devolver a < b ? b : a ; }
Esta definición de función única funciona con muchos tipos de datos. Específicamente, funciona con todos los tipos de datos para los que se define < (el operador menor que) y devuelve un valor con un tipo convertible a bool
. El uso de una plantilla de función ahorra espacio en el archivo de código fuente, además de limitar los cambios a una descripción de función y hacer que el código sea más fácil de leer.
Sin embargo, una plantilla de función instanciada generalmente produce el mismo código objeto, en comparación con escribir funciones separadas para todos los diferentes tipos de datos utilizados en un programa específico. Por ejemplo, si un programa usa tanto una int
como una double
versión de la max()
plantilla de función anterior, el compilador creará una versión de código objeto de max()
que opera sobre int
argumentos y otra versión de código objeto que opera sobre double
argumentos. [ cita requerida ] La salida del compilador será idéntica a la que se habría producido si el código fuente hubiera contenido dos versiones separadas sin plantilla de max()
, una escrita para handle int
y otra escrita para handle double
.
Así es como se podría utilizar la plantilla de función:
#include <flujo de datos> int main () { // Esto llamará a max<int> por deducción de argumento implícita. std :: cout << std :: max ( 3 , 7 ) << '\n' ; // Esto llamará a max<double> por deducción de argumento implícita. std :: cout << std :: max ( 3.0 , 7.0 ) << '\n' ; // Necesitamos especificar explícitamente el tipo de los argumentos; // aunque std::type_identity podría resolver este problema... std :: cout << max < double > ( 3 , 7.0 ) << '\n' ; }
En los dos primeros casos, T
el compilador deduce automáticamente que el argumento de plantilla es int
y double
, respectivamente. En el tercer caso, la deducción automática de max(3, 7.0)
fallaría porque el tipo de los parámetros debe coincidir exactamente con los argumentos de plantilla. Por lo tanto, instanciamos explícitamente la double
versión con max<double>()
.
Esta plantilla de función se puede instanciar con cualquier tipo que se pueda copiar y construir para el cual la expresión y < x
sea válida. Para los tipos definidos por el usuario, esto implica que el operador menor que ( <
) debe estar sobrecargado en el tipo.
Desde C++20 , al utilizar auto
o Concept auto
en cualquiera de los parámetros de una declaración de función , esa declaración se convierte en una declaración de plantilla de función abreviada . [3] Dicha declaración declara una plantilla de función y se agrega a la lista de parámetros de plantilla un parámetro de plantilla inventado para cada marcador de posición:
void f1 ( auto ); // igual que plantilla<class T> void f1(T) void f2 ( C1 auto ); // igual que plantilla<C1 T> void f2(T), si C1 es un concepto void f3 ( C2 auto ...); // igual que plantilla<C2... Ts> void f3(Ts...), si C2 es un concepto void f4 ( C2 auto , ...); // igual que plantilla<C2 T> void f4(T...), si C2 es un concepto void f5 ( const C3 auto * , C4 auto & ); // igual que plantilla<C3 T, C4 U> void f5(const T*, U&);
Una plantilla de clase proporciona una especificación para generar clases basadas en parámetros. Las plantillas de clase se utilizan generalmente para implementar contenedores . Una plantilla de clase se instancia pasándole un conjunto dado de tipos como argumentos de plantilla. [4] La biblioteca estándar de C++ contiene muchas plantillas de clase, en particular los contenedores adaptados de la biblioteca de plantillas estándar , como vector
.
En C++14, las plantillas también se pueden utilizar para variables, como en el siguiente ejemplo:
plantilla < typename T > constexpr T pi = T { 3.141592653589793238462643383L }; // (Casi) de std::numbers::pi
Aunque la creación de plantillas sobre tipos, como en los ejemplos anteriores, es la forma más común de creación de plantillas en C++, también es posible crear plantillas sobre valores. Así, por ejemplo, una clase declarada con
plantilla < int K > clase MiClase ;
se puede instanciar con un específico int
.
Como ejemplo del mundo real, el tipo de matriz de tamaño fijo de la biblioteca estándar tiene como plantilla tanto un tipo (que representa el tipo de objeto que contiene la matriz) como un número que es de tipo (que representa la cantidad de elementos que contiene la matriz). se puede declarar de la siguiente manera:std::array
std::size_t
std::array
plantilla < clase T , tamaño_t N > estructura matriz ;
char
y se podría declarar una matriz de seis s:
matriz < char , 6 > myArray ;
Cuando se crea una instancia de una función o clase a partir de una plantilla, el compilador crea una especialización de esa plantilla para el conjunto de argumentos utilizados, y la especialización se denomina especialización generada.
En ocasiones, el programador puede decidir implementar una versión especial de una función (o clase) para un conjunto determinado de argumentos de tipo de plantilla, lo que se denomina especialización explícita. De esta manera, ciertos tipos de plantilla pueden tener una implementación especializada que esté optimizada para el tipo o una implementación más significativa que la implementación genérica.
La especialización explícita se utiliza cuando el comportamiento de una función o clase para determinadas opciones de los parámetros de plantilla debe desviarse del comportamiento genérico: es decir, del código generado por la plantilla principal o las plantillas. Por ejemplo, la definición de plantilla que aparece a continuación define una implementación específica de max()
para argumentos de tipo const char*
:
#include <cadena de caracteres> plantilla <> const char * max ( const char * a , const char * b ) { // Normalmente, el resultado de una comparación directa // entre dos cadenas C es un comportamiento indefinido; // el uso de std::strcmp hace que se defina. return std :: strcmp ( a , b ) > 0 ? a : b ; }
C++11 introdujo plantillas variádicas , que pueden tomar una cantidad variable de argumentos de una manera similar a las funciones variádicas como std::printf
.
C++11 introdujo alias de plantilla, que actúan como definiciones de tipos parametrizadas .
El código siguiente muestra la definición de un alias de plantilla StrMap
. Esto permite, por ejemplo, StrMap<int>
utilizarlo como abreviatura de std::unordered_map<int,std::string>
.
plantilla < typename T > usando StrMap = std :: unordered_map < T , std :: string > ;
Inicialmente, el concepto de plantillas no se incluyó en algunos lenguajes, como Java y C# 1.0. La adopción de genéricos por parte de Java imita el comportamiento de las plantillas, pero es técnicamente diferente. C# agregó genéricos (tipos parametrizados) en .NET 2.0. Los genéricos en Ada son anteriores a las plantillas de C++.
Aunque las plantillas de C++, los genéricos de Java y los genéricos de .NET a menudo se consideran similares, los genéricos solo imitan el comportamiento básico de las plantillas de C++ . [5] Algunas de las características de plantilla avanzadas utilizadas por bibliotecas como Boost y STLSoft, e implementaciones de STL, para la metaprogramación de plantillas (especialización explícita o parcial, argumentos de plantilla predeterminados, argumentos de plantilla que no son de tipo, argumentos de plantilla de plantilla, ...) no están disponibles con los genéricos.
En las plantillas de C++, los casos de tiempo de compilación se realizaban históricamente mediante la coincidencia de patrones sobre los argumentos de la plantilla. Por ejemplo, la clase base de plantilla en el ejemplo de Factorial que se muestra a continuación se implementa mediante la coincidencia de 0 en lugar de con una prueba de desigualdad, que anteriormente no estaba disponible. Sin embargo, la llegada a C++11 de funciones de la biblioteca estándar como std::conditional ha proporcionado otra forma más flexible de gestionar la instanciación de plantillas condicionales.
// Plantilla de inducción < unsigned N > struct Factorial { static constexpr unsigned value = N * Factorial < N - 1 >:: value ; }; // Caso base a través de la especialización de plantillas: plantilla <> struct Factorial < 0 > { static constexpr unsigned value = 1 ; };
Con estas definiciones, se puede calcular, digamos, 6! en tiempo de compilación utilizando la expresión Factorial<6>::value
.
Alternativamente, constexpr
en C++11 / if constexpr
en C++17 se puede calcular dichos valores directamente usando una función en tiempo de compilación:
plantilla < unsigned N > unsigned factorial () { si constexpr ( N <= 1 ) devuelve 1 ; de lo contrario devuelve N * factorial < N -1 > (); }
Debido a esto, la metaprogramación de plantillas ahora se utiliza principalmente para realizar operaciones con tipos.