Plantillas (C++)

Las plantillas son la base para la programación genérica en C++. Como lenguaje fuertemente tipado, C++ requiere que todas las variables tengan un tipo específico, ya sea declarado explícitamente por el programador o deducido por el compilador. Sin embargo, muchas estructuras de datos y algoritmos tienen el mismo aspecto independientemente del tipo en el que operan. Las plantillas permiten definir las operaciones de una clase o función y permiten al usuario especificar en qué tipos concretos deben funcionar esas operaciones.

Definición y uso de plantillas

Una plantilla es una construcción que genera un tipo o función normal en tiempo de compilación en función de los argumentos que proporciona el usuario para los parámetros de la plantilla. Por ejemplo, se puede definir una plantilla de función como esta:

template <typename T>
T minimum(const T& lhs, const T& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

En el código anterior se describe una plantilla para una función genérica con un único parámetro de tipo T, cuyo valor devuelto y parámetros de llamada (lhs y rhs) son todos de este tipo. Se puede asignar un nombre a un parámetro de tipo como se desee, pero por convención se usan letras mayúsculas únicas. T es un parámetro de plantilla; la palabra clave typename dice que este parámetro es un marcador de posición para un tipo. Cuando se llama a la función, el compilador reemplazará cada instancia de T por el argumento de tipo concreto especificado por el usuario o deducido por el compilador. El proceso en el que el compilador genera una clase o función a partir de una plantilla se conoce como creación de una instancia de plantilla; minimum<int> es una creación de instancias de la plantilla minimum<T>.

En otro lugar, un usuario puede declarar una instancia de la plantilla especializada para int. Suponiéndose que get_a() y get_b() son funciones que devuelven un valor int:

int a = get_a();
int b = get_b();
int i = minimum<int>(a, b);

Sin embargo, dado que se trata de una plantilla de función y el compilador puede deducir el tipo T de los argumentos a y b, se puede llamar igual que una función normal:

int i = minimum(a, b);

Cuando el compilador encuentra esa última instrucción, genera una nueva función en la que cada aparición de T en la plantilla se reemplaza por int:

int minimum(const int& lhs, const int& rhs)
{
    return lhs < rhs ? lhs : rhs;
}

Las reglas de cómo el compilador realiza la deducción de tipos en las plantillas de función se basan en las reglas de las funciones normales. Para obtener más información, consulte Resolución de sobrecarga de las llamadas de la plantilla de función.

Parámetros de tipo

En la plantilla minimum anterior, tenga en cuenta que el parámetro de tipo T no está calificado de ninguna manera hasta que se usa en los parámetros de llamada de función, donde se agregan los calificadores de referencia y const.

No hay ningún límite práctico para el número de parámetros de tipo genérico. Separe los distintos parámetros con comas:

template <typename T, typename U, typename V> class Foo{};

La palabra clave class es equivalente a typename en este contexto. Puede expresar el ejemplo anterior como:

template <class T, class U, class V> class Foo{};

Puede usar el operador de puntos suspensivos (...) para definir una plantilla que toma un número arbitrario de cero o más parámetros de tipo:

template<typename... Arguments> class vtclass;

vtclass< > vtinstance1;
vtclass<int> vtinstance2;
vtclass<float, bool> vtinstance3;

Cualquier tipo integrado o definido por el usuario se puede usar como un argumento de tipo. Por ejemplo, se puede usar std::vector en la Biblioteca Estándar para almacenar las variables de tipo int, double, std::string, MyClass, constMyClass*, MyClass&, etc. La restricción principal al usar plantillas es que un argumento de tipo debe admitir las operaciones que se aplican a los parámetros de tipo. Por ejemplo, si llamamos a minimum usando MyClass como en este ejemplo:

class MyClass
{
public:
    int num;
    std::wstring description;
};

int main()
{
    MyClass mc1 {1, L"hello"};
    MyClass mc2 {2, L"goodbye"};
    auto result = minimum(mc1, mc2); // Error! C2678
}

Se generará un error del compilador porque MyClass no proporciona una sobrecarga para el operador <.

No hay ningún requisito inherente de que los argumentos de tipo para cualquier plantilla determinada pertenezcan a la misma jerarquía de objetos, aunque se puede definir una plantilla que aplique dicha restricción. Se pueden combinar técnicas orientadas a objetos con plantillas; por ejemplo, se puede almacenar un objeto Derived* en un vector<Base*>. Tenga en cuenta que los argumentos deben ser punteros

vector<MyClass*> vec;
   MyDerived d(3, L"back again", time(0));
   vec.push_back(&d);

   // or more realistically:
   vector<shared_ptr<MyClass>> vec2;
   vec2.push_back(make_shared<MyDerived>());

Los requisitos básicos que std::vector y otros contenedores de la biblioteca estándar imponen a los elementos de T, es que estos T se puedan copiar y construir.

Parámetros que no son de tipo

A diferencia de los tipos genéricos de otros lenguajes, como C# y Java, las plantillas de C++ admiten parámetros que no son de tipo que también son denominados parámetros de valor. Por ejemplo, se puede proporcionar un valor entero constante para especificar la longitud de una matriz, como en este ejemplo similar a la clase std::array de la Biblioteca Estándar:

template<typename T, size_t L>
class MyArray
{
    T arr[L];
public:
    MyArray() { ... }
};

Tenga en cuenta la sintaxis de la declaración de la plantilla. El valor size_t se pasa como argumento de la plantilla en tiempo de compilación y debe ser const o una expresión constexpr. Se usa de la siguiente manera:

MyArray<MyClass*, 10> arr;

Otros tipos de valores, incluidos punteros y referencias, se pueden pasar como parámetros que no son de tipo. Por ejemplo, se puede pasar un puntero a una función o un objeto de función para personalizar alguna operación dentro del código de plantilla.

Deducción de tipos para los parámetros de plantilla que no son de tipo

En Visual Studio 2017 y versiones posteriores, además del modo /std:c++17 o posterior, el compilador deduce el tipo de un argumento de la plantilla que no es de tipo declarado con auto:

template <auto x> constexpr auto constant = x;

auto v1 = constant<5>;      // v1 == 5, decltype(v1) is int
auto v2 = constant<true>;   // v2 == true, decltype(v2) is bool
auto v3 = constant<'a'>;    // v3 == 'a', decltype(v3) is char

Plantillas como parámetros de plantilla

Una plantilla puede ser un parámetro de plantilla. En este ejemplo, MyClass2 tiene dos parámetros de plantilla: un parámetro typename T y un parámetro de plantilla Arr:

template<typename T, template<typename U, int I> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
    U u; //Error. U not in scope
};

Dado que el propio parámetro Arr no tiene cuerpo, sus nombres de parámetro no son necesarios. De hecho, es un error hacer referencia a los nombres de parámetro de clase o el typename de Arr desde el cuerpo de MyClass2. Por este motivo, se pueden omitir los nombres de parámetros de tipo Arr, como se muestra en este ejemplo:

template<typename T, template<typename, int> class Arr>
class MyClass2
{
    T t; //OK
    Arr<T, 10> a;
};

Argumentos de plantilla predeterminados

Las plantillas de clase y función pueden tener argumentos predeterminados. Cuando una plantilla tiene un argumento predeterminado, se puede dejar sin especificar al usarlo. Por ejemplo, la plantilla std::vector tiene un argumento predeterminado para el asignador:

template <class T, class Allocator = allocator<T>> class vector;

En la mayoría de los casos, la clase std::allocator predeterminada es aceptable, por lo que se usa un vector similar al siguiente:

vector<int> myInts;

Pero si es necesario, se puede especificar un asignador personalizado de la siguiente manera:

vector<int, MyAllocator> ints;

Para varios argumentos de plantilla, todos los argumentos después del primer argumento predeterminado deben tener argumentos predeterminados.

Cuando se usa una plantilla cuyos parámetros están todos predeterminados, se usan corchetes angulares vacíos:

template<typename A = int, typename B = double>
class Bar
{
    //...
};
...
int main()
{
    Bar<> bar; // use all default type arguments
}

Especialización de plantilla

En algunos casos, no es posible o deseable que una plantilla defina exactamente el mismo código para cualquier tipo. Por ejemplo, es posible que se desee definir una ruta de acceso de código que se va a ejecutar solo si el argumento de tipo es un puntero o un std::wstring, o un tipo derivado de una clase base determinada. En tales casos, se puede definir una especialización de la plantilla para ese tipo determinado. Cuando un usuario crea una instancia de la plantilla con ese tipo, el compilador usa la especialización para generar la clase y para todos los demás tipos, el compilador elige la plantilla más general. Las especializaciones en las que todos los parámetros están especializados son especializaciones completas. Si solo algunos de los parámetros están especializados, se denomina especialización parcial.

template <typename K, typename V>
class MyMap{/*...*/};

// partial specialization for string keys
template<typename V>
class MyMap<string, V> {/*...*/};
...
MyMap<int, MyClass> classes; // uses original template
MyMap<string, MyClass> classes2; // uses the partial specialization

Una plantilla puede tener cualquier número de especializaciones siempre que cada parámetro de tipo especializado sea único. Solo las plantillas de clase pueden estar parcialmente especializadas. Todas las especializaciones completas y parciales de una plantilla deben declararse en el mismo espacio de nombres que la plantilla original.

Para obtener más información, consulte Especificaciones de plantilla.