Templates de clase: Definición, miembros y especialización

Un template de clase no es una clase en sí misma, sino una receta para que el compilador genere clases reales. Cuando declaras template<typename T> class Container, no estás definiendo un tipo, sino una instrucción para que el compilador fabrique un tipo cada vez que uses Container<int>, Container<double>, etc. Este proceso se llama instanciación.

Para que esto funcione, el compilador necesita tener la definición completa del template disponible en cada unidad de traducción donde se use. El diseño de los templates permite manejar tanto tipos (typename) como valores constantes en tiempo de compilación, conocidos como parámetros de plantilla no-tipo [C++11], como un int o un std::size_t. Esto es crucial para optimizaciones de rendimiento, como definir el tamaño de un array estático dentro de la clase. Si intentas usar un parámetro que no sea una constante integral, una referencia o un puntero, el compilador fallará porque los parámetros de plantilla no-tipo deben ser evaluables en tiempo de compilación.

Sin embargo, no todo es siempre genérico. A veces, un tipo específico requiere un comportamiento radicalmente distinto. Aquí es donde entra la especialización completa. Si tu contenedor de tipo T funciona de maravilla para int, pero quieres que para bool use un std::bitset para ahorrar memoria, puedes definir una especialización que ignore la lógica general y aplique la optimización específica.

Si no tienes cuidado con la gestión de estos templates, puedes caer en dos problemas: el bloat de binario (generar cientos de versiones de la misma clase con ligeras variaciones de tipos) o errores de ODR (One Definition Rule). Para controlar esto, usamos la instanciación explícita para forzar la creación de un tipo en un archivo específico, y extern template para decirle al compilador: “no instancies esto aquí, ya está definido en otro archivo”, evitando que el compilador trabaje de más en cada unidad de traducción.

#include <iostream>
#include <type_traits>
#include <bitset>
#include <vector>

// Un simple contenedor para demostrar template template parameters
template<typename T>
class SimpleWrapper {
    T data;
public:
    explicit SimpleWrapper(T val) : data(val) {}
    void print() const { std::cout << "Wrapper: " << data << "\n"; }
};

// 1. Definición de clase template con:
//    - Parámetro de tipo (T)
//    - Parámetro no-tipo (N)
//    - Parámetro con valor por defecto (Allocator)
template<typename T, std::size_t N, typename Allocator = std::allocator<T>>
class FixedBuffer {
protected:
    T storage[N];
    std::size_t current_size = 0;

public:
    FixedBuffer() = default;
    
    // Prototipo de miembro definido fuera de la clase
    void push_back(const T& value);

    std::size_t size() const { return current_size; }
};

// 2. Definición de miembro fuera de la clase:
// Requiere repetir la lista completa de parámetros del template.
template<typename T, std::size_t N, typename Allocator>
void FixedBuffer<T, N, Allocator>::push_back(const T& value) {
    if (current_size < N) {
        storage[current_size++] = value;
    }
}

// 3. Especialización completa para bool:
// Se optimiza usando std::bitset en lugar de un array de bool (que suele usar 1 byte por bool).
// Nota: En una especialización completa, debemos especificar todos los parámetros.
template<std::size_t N, typename Allocator>
class FixedBuffer<bool, N, Allocator> {
    std::bitset<N> bits;
    std::size_t count = 0;
public:
    void push_back(const bool value) {
        if (count < N) {
            bits.set(count, value);
            count++;
        }
    }
    std::size_t size() const { return count; }
    void print() const { std::cout << "Bitset: " << bits << "\n"; }
};

// 4. Template de plantilla (Template Template Parameter):
// Recibe un template como parámetro (en este caso, un contenedor que acepte un tipo).
template<template<typename> class Container>
class ContainerProxy {
    Container<int> internal_container;
public:
    void add(int val) { internal_container.push_back(val); }
    void show() const { std::cout << "Proxy wrapping a container.\n"; }
};

// 5. Instanciación explícita: Fuerza la creación de esta versión específica.
template class FixedBuffer<int, 64>;

// 6. Extern template: Le dice al compilador que no instancie esto aquí, 
// porque ya se hizo en otra unidad de traducción (evita redundancia).
extern template class FixedBuffer<double, 128>;

int main() {
    // Uso estándar
    FixedBuffer<int, 5> intBuffer;
    intBuffer.push_back(10);
    intBuffer.push_back(20);

    // Uso de la especialización para bool
    FixedBuffer<bool, 8> boolBuffer;
    boolBuffer.push_back(true);
    boolBuffer.push_back(false);
    boolBuffer.push_back(true);

    // Uso de Template Template Parameter
    ContainerProxy<std::vector> proxy;
    proxy.add(100);
    proxy.show();

    return 0;
}

Desglose del código

  • FixedBuffer<T, N, Allocator>: La clase utiliza tres parámetros. T es el tipo de dato, N es un parámetro no-tipo (std::size_t), lo que permite que el tamaño del array storage[N] se determine en tiempo de compilación, permitiendo que el compilador optimice el acceso a memoria. Allocator tiene un valor por defecto, permitiendo usar FixedBuffer<int, 10> sin especificar el asignador.
  • FixedBuffer<T, N, Allocator>::push_back: Al definir la función fuera de la clase, es obligatorio repetir toda la firma del template: template<typename T, std::size_t N, typename Allocator>. Si olvidaras N o Allocator, el compilador buscaría una versión de la clase que no existe.
  • Especialización de bool: Hemos creado una versión de FixedBuffer optimizada para bool. Al usar std::bitset<N>, el uso de memoria es de 1 bit por elemento en lugar de 8 bits (el tamaño habitual de un bool en la mayoría de las arquitecturas). Esta es una especialización completa porque redefine completamente la interfaz y la estructura interna para ese tipo.
  • ContainerProxy: Este es un ejemplo de template de plantilla. El parámetro Container no es un tipo, sino un nombre de un template (como std::vector). Esto permite que ContainerProxy sea un adaptador genérico para cualquier contenedor que acepte un tipo de parámetro.
  • Instanciación y extern: template class FixedBuffer<int, 64>; le dice al compilador: “No esperes a que alguien use FixedBuffer<int, 64> para crear el código; genéralo ahora mismo”. Por el contrario, extern template ... es una instrucción de optimización para proyectos grandes, evitando que cada archivo .cpp genere su propia copia de la misma clase especializada, reduciendo el tiempo de enlace y el tamaño del binario.

El error frecuente

Un error clásico al trabajar con funciones miembro definidas fuera de la clase es la inconsistencia en la firma del template.

template<typename T, int N>
class MyArray {
    void set(int index, T val);
};

// ERROR: El compilador pensará que esto es una función normal, no un miembro de un template
template<typename T> 
void MyArray<T>::set(int index, T val) { // Error de compilación o linkeo
    // ...
}

Si la clase tiene dos parámetros (T y N), la definición del método debe incluir ambos en la lista de parámetros del template y en la resolución de ámbito MyArray<T, N>::. De lo contrario, el compilador no podrá encontrar la especialización correcta y lanzará un error indicando que set no es un miembro de MyArray<T>. Para detectar estos errores de tipo o de firma, utiliza siempre el flag -Wextra y, si es posible, activa los sanitizadores o las advertencias de -Werror en tu pipeline de CI.

49

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio