Buscar..
Introducción
Las clases, funciones y (desde C ++ 14) las variables pueden tener plantillas. Una plantilla es un fragmento de código con algunos parámetros libres que se convertirán en una clase, función o variable concreta cuando se especifiquen todos los parámetros. Los parámetros pueden ser tipos, valores o plantillas. Una plantilla conocida es std::vector
, que se convierte en un tipo de contenedor concreto cuando se especifica el tipo de elemento, por ejemplo, std::vector<int>
.
Sintaxis
- declaración de plantilla < plantilla-lista-parámetros >
- exportar plantilla < lista de parámetros de plantilla > declaración / * hasta C ++ 11 * /
- plantilla <> declaración
- declaración de plantilla
- Declaración de plantilla externa / * desde C ++ 11 * /
- plantilla < plantilla-lista-parámetros > clase ... ( opt ) identificador ( opt )
- plantilla < plantilla-lista-parámetros > identificador de clase ( opt ) = id-expresión
- template < template-parameters-list > typename ... ( opt ) identifier ( opt ) / * desde C ++ 17 * /
- plantilla < plantilla-lista-parámetros > identificador de nombre de tipo ( opt ) = id-expresión / * desde C ++ 17 * /
- postfix-expresión . expresión- plantilla de la plantilla
- postfix-expresión -> plantilla id-expresión
-
template
especificador de nombre anidado simple-template-id::
Observaciones
La palabra template
es una palabra clave con cinco significados diferentes en el lenguaje C ++, dependiendo del contexto.
Cuando sigue una lista de parámetros de plantilla incluida en
<>
, declara una plantilla como una plantilla de clase , una plantilla de función o una especialización parcial de una plantilla existente.template <class T> void increment(T& x) { ++x; }
Cuando es seguido por un vacío
<>
, declara una especialización explícita (completa) .template <class T> void print(T x); template <> // <-- keyword used in this sense here void print(const char* s) { // output the content of the string printf("%s\n", s); }
Cuando sigue una declaración sin
<>
, forma una declaración o definición de instanciación explícita .template <class T> std::set<T> make_singleton(T x) { return std::set<T>(x); } template std::set<int> make_singleton(int x); // <-- keyword used in this sense here
Dentro de una lista de parámetros de plantilla, introduce un parámetro de plantilla de plantilla .
template <class T, template <class U> class Alloc> // ^^^^^^^^ keyword used in this sense here class List { struct Node { T value; Node* next; }; Alloc<Node> allocator; Node* allocate_node() { return allocator.allocate(sizeof(T)); } // ... };
Después del operador de resolución de alcance
::
y los operadores de acceso de miembros de clase.
y->
, especifica que el siguiente nombre es una plantilla.struct Allocator { template <class T> T* allocate(); }; template <class T, class Alloc> class List { struct Node { T value; Node* next; } Alloc allocator; Node* allocate_node() { // return allocator.allocate<Node>(); // error: < and > are interpreted as // comparison operators return allocator.template allocate<Node>(); // ok; allocate is a template // ^^^^^^^^ keyword used in this sense here } };
Antes de C ++ 11, se podría declarar una plantilla con la palabra clave de export
, convirtiéndola en una plantilla exportada . La definición de una plantilla exportada no necesita estar presente en cada unidad de traducción en la que se crea una instancia de la plantilla. Por ejemplo, se suponía que funcionaba lo siguiente:
foo.h
:
#ifndef FOO_H
#define FOO_H
export template <class T> T identity(T x);
#endif
foo.cpp
:
#include "foo.h"
template <class T> T identity(T x) { return x; }
main.cpp
:
#include "foo.h"
int main() {
const int x = identity(42); // x is 42
}
Debido a la dificultad de implementación, la palabra clave de export
no era compatible con la mayoría de los compiladores principales. Se eliminó en C ++ 11; ahora, es ilegal utilizar la palabra clave de export
en absoluto. En su lugar, normalmente es necesario definir plantillas en los encabezados (a diferencia de las funciones que no son de plantilla, que generalmente no están definidas en los encabezados). Consulte ¿Por qué las plantillas solo se pueden implementar en el archivo de encabezado?
Plantillas de funciones
Las plantillas también se pueden aplicar a las funciones (así como a las estructuras más tradicionales) con el mismo efecto.
// 'T' stands for the unknown type
// Both of our arguments will be of the same type.
template<typename T>
void printSum(T add1, T add2)
{
std::cout << (add1 + add2) << std::endl;
}
Esto puede ser usado de la misma manera que las plantillas de estructura.
printSum<int>(4, 5);
printSum<float>(4.5f, 8.9f);
En ambos casos, el argumento de la plantilla se utiliza para reemplazar los tipos de parámetros; el resultado funciona igual que una función normal de C ++ (si los parámetros no coinciden con el tipo de plantilla, el compilador aplica las conversiones estándar).
Una propiedad adicional de las funciones de plantilla (a diferencia de las clases de plantilla) es que el compilador puede inferir los parámetros de la plantilla en función de los parámetros pasados a la función.
printSum(4, 5); // Both parameters are int.
// This allows the compiler deduce that the type
// T is also int.
printSum(5.0, 4); // In this case the parameters are two different types.
// The compiler is unable to deduce the type of T
// because there are contradictions. As a result
// this is a compile time error.
Esta característica nos permite simplificar el código cuando combinamos estructuras de plantillas y funciones. Hay un patrón común en la biblioteca estándar que nos permite crear una template structure X
mediante la función auxiliar make_X()
.
// The make_X pattern looks like this.
// 1) A template structure with 1 or more template types.
template<typename T1, typename T2>
struct MyPair
{
T1 first;
T2 second;
};
// 2) A make function that has a parameter type for
// each template parameter in the template structure.
template<typename T1, typename T2>
MyPair<T1, T2> make_MyPair(T1 t1, T2 t2)
{
return MyPair<T1, T2>{t1, t2};
}
¿Cómo ayuda esto?
auto val1 = MyPair<int, float>{5, 8.7}; // Create object explicitly defining the types
auto val2 = make_MyPair(5, 8.7); // Create object using the types of the paramters.
// In this code both val1 and val2 are the same
// type.
Nota: Esto no está diseñado para acortar el código. Esto está diseñado para hacer que el código sea más robusto. Permite cambiar los tipos cambiando el código en un solo lugar en lugar de en varias ubicaciones.
Reenvío de argumentos
La plantilla puede aceptar tanto las referencias lvalue como rvalue usando la referencia de reenvío :
template <typename T>
void f(T &&t);
En este caso, el tipo real de t
se deducirá según el contexto:
struct X { };
X x;
f(x); // calls f<X&>(x)
f(X()); // calls f<X>(x)
En el primer caso, el tipo T
se deduce como referencia a X
( X&
), y el tipo de t
es la referencia lvalue a X
, mientras que en el segundo caso el tipo de T
se deduce como X
y el tipo de t
como referencia rvalue a X
( X&&
).
Nota: vale la pena notar que en el primer caso, decltype(t)
es lo mismo que T
, pero no en el segundo.
Para reenviar perfectamente t
a otra función, ya sea una referencia de lvalue o rvalue, se debe usar std::forward
:
template <typename T>
void f(T &&t) {
g(std::forward<T>(t));
}
Las referencias de reenvío se pueden utilizar con las plantillas variadic:
template <typename... Args>
void f(Args&&... args) {
g(std::forward<Args>(args)...);
}
Nota: las referencias de reenvío solo se pueden usar para los parámetros de la plantilla, por ejemplo, en el siguiente código, v
es una referencia de valor, no una referencia de reenvío:
#include <vector>
template <typename T>
void f(std::vector<T> &&v);
Plantilla de clase básica
La idea básica de una plantilla de clase es que el parámetro de plantilla se sustituye por un tipo en tiempo de compilación. El resultado es que la misma clase se puede reutilizar para varios tipos. El usuario especifica qué tipo se utilizará cuando se declara una variable de la clase. Tres ejemplos de esto se muestran en main()
:
#include <iostream>
using std::cout;
template <typename T> // A simple class to hold one number of any type
class Number {
public:
void setNum(T n); // Sets the class field to the given number
T plus1() const; // returns class field's "follower"
private:
T num; // Class field
};
template <typename T> // Set the class field to the given number
void Number<T>::setNum(T n) {
num = n;
}
template <typename T> // returns class field's "follower"
T Number<T>::plus1() const {
return num + 1;
}
int main() {
Number<int> anInt; // Test with an integer (int replaces T in the class)
anInt.setNum(1);
cout << "My integer + 1 is " << anInt.plus1() << "\n"; // Prints 2
Number<double> aDouble; // Test with a double
aDouble.setNum(3.1415926535897);
cout << "My double + 1 is " << aDouble.plus1() << "\n"; // Prints 4.14159
Number<float> aFloat; // Test with a float
aFloat.setNum(1.4);
cout << "My float + 1 is " << aFloat.plus1() << "\n"; // Prints 2.4
return 0; // Successful completion
}
Especialización en plantillas
Puede definir la implementación para instancias específicas de una clase de plantilla / método.
Por ejemplo si tienes:
template <typename T>
T sqrt(T t) { /* Some generic implementation */ }
A continuación, puede escribir:
template<>
int sqrt<int>(int i) { /* Highly optimized integer implementation */ }
Luego, un usuario que escribe sqrt(4.0)
obtendrá la implementación genérica, mientras que sqrt(4)
obtendrá la implementación especializada.
Especialización en plantillas parciales.
A diferencia de una especialización de plantilla completa, la especialización de plantilla parcial permite introducir una plantilla con algunos de los argumentos de la plantilla existente corregidos. La especialización de plantilla parcial solo está disponible para la clase de plantilla / estructuras:
// Common case:
template<typename T, typename U>
struct S {
T t_val;
U u_val;
};
// Special case when the first template argument is fixed to int
template<typename V>
struct S<int, V> {
double another_value;
int foo(double arg) {// Do something}
};
Como se muestra arriba, las especializaciones de plantillas parciales pueden introducir conjuntos de datos y funciones completamente diferentes.
Cuando se crea una instancia de una plantilla parcialmente especializada, se selecciona la especialización más adecuada. Por ejemplo, definamos una plantilla y dos especializaciones parciales:
template<typename T, typename U, typename V>
struct S {
static void foo() {
std::cout << "General case\n";
}
};
template<typename U, typename V>
struct S<int, U, V> {
static void foo() {
std::cout << "T = int\n";
}
};
template<typename V>
struct S<int, double, V> {
static void foo() {
std::cout << "T = int, U = double\n";
}
};
Ahora las siguientes llamadas:
S<std::string, int, double>::foo();
S<int, float, std::string>::foo();
S<int, double, std::string>::foo();
imprimirá
General case
T = int
T = int, U = double
Las plantillas de funciones solo pueden ser totalmente especializadas:
template<typename T, typename U>
void foo(T t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
// OK.
template<>
void foo<int, int>(int a1, int a2) {
std::cout << "Two ints: " << a1 << " " << a2 << std::endl;
}
void invoke_foo() {
foo(1, 2.1); // Prints "General case: 1 2.1"
foo(1,2); // Prints "Two ints: 1 2"
}
// Compilation error: partial function specialization is not allowed.
template<typename U>
void foo<std::string, U>(std::string t, U u) {
std::cout << "General case: " << t << " " << u << std::endl;
}
Valor predeterminado del parámetro de la plantilla
Al igual que en el caso de los argumentos de la función, los parámetros de la plantilla pueden tener sus valores predeterminados. Todos los parámetros de plantilla con un valor predeterminado deben declararse al final de la lista de parámetros de plantilla. La idea básica es que los parámetros de la plantilla con el valor predeterminado se pueden omitir mientras se crea una instancia de la plantilla.
Ejemplo simple de uso de valor de parámetro de plantilla predeterminado:
template <class T, size_t N = 10>
struct my_array {
T arr[N];
};
int main() {
/* Default parameter is ignored, N = 5 */
my_array<int, 5> a;
/* Print the length of a.arr: 5 */
std::cout << sizeof(a.arr) / sizeof(int) << std::endl;
/* Last parameter is omitted, N = 10 */
my_array<int> b;
/* Print the length of a.arr: 10 */
std::cout << sizeof(b.arr) / sizeof(int) << std::endl;
}
Plantilla alias
Ejemplo básico:
template<typename T> using pointer = T*;
Esta definición hace al pointer<T>
un alias de T*
. Por ejemplo:
pointer<int> p = new int; // equivalent to: int* p = new int;
Las plantillas de alias no pueden ser especializadas. Sin embargo, esa funcionalidad se puede obtener indirectamente haciendo que se refieran a un tipo anidado en una estructura:
template<typename T>
struct nonconst_pointer_helper { typedef T* type; };
template<typename T>
struct nonconst_pointer_helper<T const> { typedef T* type; };
template<typename T> using nonconst_pointer = nonconst_pointer_helper<T>::type;
Plantilla plantilla parámetros
A veces nos gustaría pasar a la plantilla un tipo de plantilla sin fijar sus valores. Para esto se crean los parámetros de plantilla de plantilla. Ejemplos de parámetros de plantillas de plantillas muy simples:
template <class T>
struct Tag1 { };
template <class T>
struct Tag2 { };
template <template <class> class Tag>
struct IntTag {
typedef Tag<int> type;
};
int main() {
IntTag<Tag1>::type t;
}
#include <vector>
#include <iostream>
template <class T, template <class...> class C, class U>
C<T> cast_all(const C<U> &c) {
C<T> result(c.begin(), c.end());
return result;
}
int main() {
std::vector<float> vf = {1.2, 2.6, 3.7};
auto vi = cast_all<int>(vf);
for(auto &&i: vi) {
std::cout << i << std::endl;
}
}
Declaración de argumentos de plantilla no tipo con auto
Antes de C ++ 17, al escribir un parámetro de no tipo de plantilla, primero tenía que especificar su tipo. Así que un patrón común se convirtió en algo como:
template <class T, T N>
struct integral_constant {
using type = T;
static constexpr T value = N;
};
using five = integral_constant<int, 5>;
Pero para expresiones complicadas, usar algo como esto implica tener que escribir decltype(expr), expr
al crear instancias de plantillas. La solución es simplificar este lenguaje y simplemente permitir el auto
:
template <auto N>
struct integral_constant {
using type = decltype(N);
static constexpr type value = N;
};
using five = integral_constant<5>;
Borrador personalizado vacío para unique_ptr
Un buen ejemplo de motivación puede venir al tratar de combinar la optimización de la base vacía con un eliminador personalizado para unique_ptr
. Los diferentes borradores de la API de C tienen diferentes tipos de retorno, pero no nos importa, solo queremos que funcione para cualquier función:
template <auto DeleteFn>
struct FunctionDeleter {
template <class T>
void operator()(T* ptr) const {
DeleteFn(ptr);
}
};
template <T, auto DeleteFn>
using unique_ptr_deleter = std::unique_ptr<T, FunctionDeleter<DeleteFn>>;
Y ahora puede simplemente usar cualquier puntero de función que pueda tomar un argumento de tipo T
como un parámetro no tipo de plantilla, independientemente del tipo de retorno, y obtener una sobrecarga de tamaño unique_ptr
fuera de él:
unique_ptr_deleter<std::FILE, std::fclose> p;
Parámetro de plantilla sin tipo
Aparte de los tipos como parámetro de plantilla, podemos declarar valores de expresiones constantes que cumplan uno de los siguientes criterios:
- tipo integral o de enumeración,
- puntero a objeto o puntero a función,
- lvalue referencia a objeto o lvalue referencia a función,
- puntero al miembro,
-
std::nullptr_t
.
Al igual que todos los parámetros de la plantilla, los parámetros de la plantilla que no son de tipo se pueden especificar, predeterminar o derivar de manera explícita mediante la deducción de argumentos de la plantilla.
Ejemplo de uso de parámetros de plantilla no tipo:
#include <iostream>
template<typename T, std::size_t size>
std::size_t size_of(T (&anArray)[size]) // Pass array by reference. Requires.
{ // an exact size. We allow all sizes
return size; // by using a template "size".
}
int main()
{
char anArrayOfChar[15];
std::cout << "anArrayOfChar: " << size_of(anArrayOfChar) << "\n";
int anArrayOfData[] = {1,2,3,4,5,6,7,8,9};
std::cout << "anArrayOfData: " << size_of(anArrayOfData) << "\n";
}
Ejemplo de especificación explícita de parámetros de tipo y no de tipo de plantilla:
#include <array>
int main ()
{
std::array<int, 5> foo; // int is a type parameter, 5 is non-type
}
Los parámetros de plantilla que no son de tipo son una de las formas de lograr la recurrencia de la plantilla y permiten realizar la metaprogramación .
Estructuras de datos de plantillas variables
A menudo es útil definir clases o estructuras que tienen un número variable y un tipo de miembros de datos que se definen en el momento de la compilación. El ejemplo canónico es std::tuple
, pero a veces es necesario definir sus propias estructuras personalizadas. Aquí hay un ejemplo que define la estructura usando la composición (en lugar de la herencia como con std::tuple
. Comience con la definición general (vacía), que también sirve como el caso base para la terminación de recrusión en la especialización posterior)
template<typename ... T>
struct DataStructure {};
Esto ya nos permite definir una estructura vacía, DataStructure<> data
, aunque aún no es muy útil.
A continuación viene la especialización de casos recursivos:
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
};
Ahora es suficiente para que DataStructure<int, float, std::string> data(1, 2.1, "hello")
estructuras de datos arbitrarias, como DataStructure<int, float, std::string> data(1, 2.1, "hello")
.
Entonces, ¿qué está pasando? Primero, tenga en cuenta que esta es una especialización cuyo requisito es que exista al menos un parámetro de plantilla variable (a saber, T
arriba), mientras que no se preocupa por la composición específica del paquete Rest
. Saber que T
existe permite la definición de su miembro de datos, first
. El resto de los datos se empaquetan recursivamente como DataStructure<Rest ... > rest
. El constructor inicia ambos miembros, incluida una llamada de constructor recursiva al miembro rest
.
Para comprender mejor esto, podemos trabajar con un ejemplo: supongamos que tiene una declaración DataStructure<int, float> data
. La declaración primero coincide con la especialización, produciendo una estructura con int first
y DataStructure<float> rest
de miembros de datos. La definición de rest
coincide nuevamente con esta especialización, creando float first
su propia float first
y los DataStructure<> rest
. Finalmente, este último rest
coincide con la definición del caso base, produciendo una estructura vacía.
Puedes visualizar esto de la siguiente manera:
DataStructure<int, float>
-> int first
-> DataStructure<float> rest
-> float first
-> DataStructure<> rest
-> (empty)
Ahora tenemos la estructura de datos, pero no es terriblemente útil todavía, ya que no podemos acceder fácilmente a los elementos de datos individuales (por ejemplo, para acceder al último miembro de DataStructure<int, float, std::string> data
tendríamos que usar data.rest.rest.first
, que no es exactamente fácil de usar). Así que le agregamos un método de get
(solo necesario en la especialización, ya que la estructura del caso base no tiene datos para get
):
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
...
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
...
};
Como puede ver, esta función de get
miembros tiene su propia plantilla, esta vez en el índice del miembro que se necesita (de modo que el uso puede ser similar a data.get<1>()
, similar a std::tuple
). El trabajo real se realiza mediante una función estática en una clase auxiliar, GetHelper
. La razón por la que no podemos definir la funcionalidad requerida directamente en DataStructure
's get
es porque (como veremos dentro de poco) que tendría que especializarse en idx
- pero no es posible especializarse una función miembro de plantilla sin que se especializa la clase que contiene modelo. Tenga en cuenta que el uso de un auto
estilo C ++ 14 aquí hace que nuestras vidas sean mucho más simples, ya que de lo contrario necesitaríamos una expresión bastante complicada para el tipo de retorno.
Así que a la clase de ayuda. Esta vez necesitaremos una declaración a futuro vacía y dos especializaciones. Primero la declaración:
template<size_t idx, typename T>
struct GetHelper;
Ahora el caso base (cuando idx==0
). En este caso acabamos de devolver el first
miembro:
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
En el caso recursivo, decrementamos idx
e invocamos GetHelper
para el miembro rest
:
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
Para ver un ejemplo, supongamos que tenemos DataStructure<int, float> data
y necesitamos data.get<1>()
. Esto invoca a GetHelper<1, DataStructure<int, float>>::get(data)
(la segunda especialización), que a su vez invoca a GetHelper<0, DataStructure<float>>::get(data.rest)
, que finalmente devuelve (por la 1ª especialización como ahora idx
es 0) data.rest.first
.
¡Eso es todo! Aquí está el código de funcionamiento completo, con algunos ejemplos de uso en la función main
:
#include <iostream>
template<size_t idx, typename T>
struct GetHelper;
template<typename ... T>
struct DataStructure
{
};
template<typename T, typename ... Rest>
struct DataStructure<T, Rest ...>
{
DataStructure(const T& first, const Rest& ... rest)
: first(first)
, rest(rest...)
{}
T first;
DataStructure<Rest ... > rest;
template<size_t idx>
auto get()
{
return GetHelper<idx, DataStructure<T,Rest...>>::get(*this);
}
};
template<typename T, typename ... Rest>
struct GetHelper<0, DataStructure<T, Rest ... >>
{
static T get(DataStructure<T, Rest...>& data)
{
return data.first;
}
};
template<size_t idx, typename T, typename ... Rest>
struct GetHelper<idx, DataStructure<T, Rest ... >>
{
static auto get(DataStructure<T, Rest...>& data)
{
return GetHelper<idx-1, DataStructure<Rest ...>>::get(data.rest);
}
};
int main()
{
DataStructure<int, float, std::string> data(1, 2.1, "Hello");
std::cout << data.get<0>() << std::endl;
std::cout << data.get<1>() << std::endl;
std::cout << data.get<2>() << std::endl;
return 0;
}
Instanciación explícita
Una definición de creación de instancias explícita crea y declara una clase, función o variable concreta de una plantilla, sin usarla todavía. Una instanciación explícita puede ser referenciada desde otras unidades de traducción. Esto se puede usar para evitar definir una plantilla en un archivo de encabezado, si solo se creará una instancia con un conjunto finito de argumentos. Por ejemplo:
// print_string.h
template <class T>
void print_string(const T* str);
// print_string.cpp
#include "print_string.h"
template void print_string(const char*);
template void print_string(const wchar_t*);
Debido a que print_string<char>
y print_string<wchar_t>
se print_string<wchar_t>
instancias explícitas en print_string.cpp
, el vinculador podrá encontrarlos aunque la plantilla print_string
no esté definida en el encabezado. Si estas declaraciones de creación de instancias explícitas no estuvieran presentes, probablemente se produciría un error de vinculador. Consulte ¿Por qué las plantillas solo se pueden implementar en el archivo de encabezado?
Si una definición de creación de instancias explícita va precedida por la palabra clave extern
, se convierte en una declaración de creación de instancias explícita. La presencia de una declaración de instanciación explícita para una especialización dada evita la creación de instancias implícita de la especialización dada dentro de la unidad de traducción actual. En cambio, una referencia a esa especialización que de otro modo causaría una creación de instancias implícita puede referirse a una definición de creación de instancias explícita en la misma u otra TU.
foo.h
#ifndef FOO_H
#define FOO_H
template <class T> void foo(T x) {
// complicated implementation
}
#endif
foo.cpp
#include "foo.h"
// explicit instantiation definitions for common cases
template void foo(int);
template void foo(double);
main.cpp
#include "foo.h"
// we already know foo.cpp has explicit instantiation definitions for these
extern template void foo(double);
int main() {
foo(42); // instantiates foo<int> here;
// wasteful since foo.cpp provides an explicit instantiation already!
foo(3.14); // does not instantiate foo<double> here;
// uses instantiation of foo<double> in foo.cpp instead
}