Fundamentos de Plantillas y Programación Genérica en C++

Visión General de las Plantillas

En C++, las plantillas permiten definir funciones y clases que operan con tipos genéricos, eliminando la necesidad de duplicar código para cada tipo de dato específico. Este mecanismo es clave para la programación genérica y se divide en dos categorías principales: plantillas de clase y plantillas de función.

La declaración de una plantilla no crea una definición completa; en su lugar, proporciona un esquema. La instanciación es el proceso que genera una implementación concreta a partir de la plantilla, ya sea explícitamente (especificando el tipo) o implícitamente (deducido por el compilador durante su uso).

Plantillas de Función

Una plantilla de función define una familia de funciones que comparten la misma lógica pero pueden operar con diferentes tipos de argumentos. Por ejemplo, una plantilla para calcular el máximo entre dos valores se define así:

template <typename U>
U obtenerMaximo(const U& a, const U& b) {
    return (a > b) ? a : b;
}

Al invocar la plantilla, el compilador deduce el tipo U de los argumentos. Si los tipos no coinciden, ocurre un error de compilación, como se muestra a continuación:

int x = 1, y = 2;
obtenerMaximo(x, y);       // Correcto: ambos int
double m = 3.1, n = 4.2;
obtenerMaximo(m, n);       // Correcto: ambos double
obtenerMaximo(x, m);       // Error: tipos ambiguos

La instanciación ocurre automáticamente al usar la plantilla. Si el tipo no soporta las operaciones requeridas (por ejemplo, comparar con > para std::complex), se genera un error. Además, las plantillas se compilan dos veces: primero se verifican errores sintácticos, luego se validan las llamadas durante la instanciación.

La deducción de tipos no permite conversiones automáticas; cada argumento debe coincidir exactamente con el tipo de la plantilla. Para resolver discrepancias, se pueden usar conversiones explícitas o especificar el tipo manualmente:

obtenerMaximo(static_cast<double>(1), 2.0);  // Conversión
obtenerMaximo<double>(1, 2.0);                // Especificación explícita

Las plantillas de función pueden sobrecargarse junto con funciones no plantilla. En la resolución de sobrecarga, el compilador prioriza funciones no plantila si son viables. La lista de parámetros vacía puede forzar el uso de una plantilla, y las versiones deban declararse antes de su uso.

// Función no plantilla
inline const int& obtenerMaximo(const int& a, const int& b);
// Plantilla con dos argumentos
template <typename U>
inline const U& obtenerMaximo(const U& a, const U& b);
// Plantilla con tres argumentos
template <typename U>
inline const U& obtenerMaximo(const U& a, const U& b, const U& c);
// Ejemplos de invocación:
obtenerMaximo(7, 42, 68);   // Usa la plantilla de tres argumentos
obtenerMaximo(7.0, 42.0);   // Deducido como double
obtenerMaximo('a', 'b');     // Deducido como char
obtenerMaximo(7, 42);        // Usa la función no plantilla
obtenerMaximo<>(7, 42);      // Fuerza el uso de la plantilla
obtenerMaximo<double>(7, 42); // Especificación explícita
obtenerMaximo('a', 42.7);     // Convierte a int (función no plantilla)

Plantillas de Clase

Similar a las funciones, las plantillas de clase permiten definir clases genéricas que se instancian con tipos específicos. Los parámetros pueden incluir tipos o constantes (como int o enum). Considere una pila (stack) parametrizada:

const std::size_t TamanoPilaDefault = 1024;
template <typename U, std::size_t N = TamanoPilaDefault>
class Pila {
public:
    void insertar(const U& elemento);
    bool extraer(U& elemento);
    bool tope(U& elemento) const;
private:
    std::vector<U> datos;
    std::size_t capacidad = N;
};

En la declaración de la clase, si se hace referencia a la plantilla misma (por ejemplo, en operadores como la asignación), se debe usar el nombre completo con parámetros:

template <typename U, std::size_t N>
class Pila {
public:
    Pila(const Pila<U, N>&);               // Constructor de copia
    Pila<U, N>& operator=(const Pila<U, N>&); // Operador de asignación
};

Para definir los miembros, se usa la sintaxis de plantilla. Por ejemplo, el método insertar verifica la capacidad antes de agregar elementos:

template <typename U, std::size_t CapacidadMax>
void Pila<U, CapacidadMax>::insertar(const U& elemento) {
    if (datos.size() >= CapacidadMax) {
        // Manejo de error: pila llena
        return;
    }
    datos.push_back(elemento);
}

El método tope recupera el elemento superior sin eliminarlo:

template <typename U, std::size_t CapacidadMax>
bool Pila<U, CapacidadMax>::tope(U& elemento) const {
    if (datos.empty()) return false;
    elemento = datos.back();
    return true;
}

Etiquetas: C++ plantillas programación genérica STL template metaprogramming

Publicado el 6-2 09:12