Ir al contenido

Especializacion de Templates y Tag Dispatch

Domina la especializacion completa y parcial, aprende cuando preferir la sobrecarga, y descubre tag dispatch e if constexpr como alternativas para ramificacion en tiempo de compilacion.

Por Que Especializar?

El template primario proporciona una implementacion generica que funciona para la mayoria de los tipos. Pero a veces un tipo especifico necesita un tratamiento fundamentalmente diferente. Por ejemplo, std::vector es una especializacion que empaqueta bools en bits para eficiencia de espacio (aunque es ampliamente considerado un error en la biblioteca estandar).

La especializacion de templates te permite proporcionar una definicion alternativa para argumentos de template especificos. Hay dos tipos:

1. Especializacion completa (explicita) — fijas todos los parametros de template a tipos especificos.
2. Especializacion parcial — fijas algunos parametros o los restringes (por ejemplo, especializar para todos los tipos puntero). La especializacion parcial solo esta disponible para class templates, no para function templates.

Especializacion Completa

Una especializacion completa proporciona una implementacion completamente personalizada para un conjunto especifico de argumentos de template. La sintaxis usa template <> (una lista de parametros vacia) seguida de la clase o funcion con argumentos concretos.

full_specialization.cpp
#include <iostream>
#include <cstring>

// Primary template
template <typename T>
class Formatter {
public:
    static std::string format(const T& value) {
        return std::to_string(value);
    }
};

// Full specialization for const char*
template <>
class Formatter<const char*> {
public:
    static std::string format(const char* value) {
        return std::string("\"" ) + value + "\"";
    }
};

// Full specialization for bool
template <>
class Formatter<bool> {
public:
    static std::string format(bool value) {
        return value ? "true" : "false";
    }
};

int main() {
    std::cout << Formatter<int>::format(42)            << '\n'; // 42
    std::cout << Formatter<const char*>::format("hi")  << '\n'; // "hi"
    std::cout << Formatter<bool>::format(true)          << '\n'; // true
}

Especializacion Parcial

La especializacion parcial restringe algunos parametros de template mientras deja otros genericos. Esto es extremadamente poderoso para hacer coincidir familias de tipos — todos los punteros, todos los arrays, todos los pairs, etc. Solo los class/struct/variable templates soportan especializacion parcial.

partial_specialization.cpp
#include <iostream>
#include <string>
#include <typeinfo>

// Primary template
template <typename T>
struct TypeInfo {
    static std::string name() { return "general type"; }
};

// Partial specialization for all pointer types
template <typename T>
struct TypeInfo<T*> {
    static std::string name() {
        return "pointer to " + TypeInfo<T>::name();
    }
};

// Partial specialization for all references
template <typename T>
struct TypeInfo<T&> {
    static std::string name() {
        return "reference to " + TypeInfo<T>::name();
    }
};

// Partial specialization for arrays
template <typename T, std::size_t N>
struct TypeInfo<T[N]> {
    static std::string name() {
        return "array of " + std::to_string(N) + " " + TypeInfo<T>::name();
    }
};

int main() {
    std::cout << TypeInfo<int>::name()     << '\n'; // general type
    std::cout << TypeInfo<int*>::name()    << '\n'; // pointer to general type
    std::cout << TypeInfo<int**>::name()   << '\n'; // pointer to pointer to general type
    std::cout << TypeInfo<int[5]>::name()  << '\n'; // array of 5 general type
}
Error común

Los function templates no pueden ser parcialmente especializados — solo completamente especializados. Peor aun, las especializaciones de function templates no participan en la resolucion de sobrecarga de la manera que podrias esperar. El compilador primero selecciona el mejor template base mediante resolucion de sobrecarga, y solo entonces verifica si ese template base tiene una especializacion que coincida.

Esto lleva a un comportamiento sorprendente:

template <typename T> void f(T)   { /* #1 - primario */ }
template <typename T> void f(T*)  { /* #2 - sobrecarga para punteros */ }
template <> void f<int*>(int*)     { /* #3 - especializacion de #1 */ }

Llamar a f((int*)nullptr) selecciona #2 (la mejor coincidencia de sobrecarga), no #3 (que especializa #1, no #2). La solucion: usa sobrecarga de funciones regular en lugar de especializacion de function templates. Si necesitas ramificacion por tipo en tiempo de compilacion, usa if constexpr o concepts.

Patron Tag Dispatch

Tag dispatch usa tipos struct vacios ("tags") para seleccionar sobrecargas en tiempo de compilacion. La biblioteca estandar usa esto extensamente — std::advance despacha segun las tags de categoria de iterador. Esta fue la tecnica dominante de ramificacion en tiempo de compilacion antes de if constexpr.

tag_dispatch.cpp
#include <iostream>
#include <iterator>
#include <vector>
#include <list>

namespace detail {
    // Tags are empty structs used purely for overload selection
    template <typename Iter>
    void advance_impl(Iter& it, int n, std::random_access_iterator_tag) {
        it += n;  // O(1) — random access
        std::cout << "random access advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::bidirectional_iterator_tag) {
        // O(n) — step one at a time, forward or backward
        while (n > 0) { ++it; --n; }
        while (n < 0) { --it; ++n; }
        std::cout << "bidirectional advance\n";
    }

    template <typename Iter>
    void advance_impl(Iter& it, int n, std::input_iterator_tag) {
        while (n > 0) { ++it; --n; }
        std::cout << "input iterator advance\n";
    }
}

template <typename Iter>
void my_advance(Iter& it, int n) {
    // iterator_traits extracts the category tag
    detail::advance_impl(it, n,
        typename std::iterator_traits<Iter>::iterator_category{});
}

int main() {
    std::vector<int> v{1, 2, 3, 4, 5};
    auto vit = v.begin();
    my_advance(vit, 3);  // random access advance

    std::list<int> l{1, 2, 3, 4, 5};
    auto lit = l.begin();
    my_advance(lit, 3);  // bidirectional advance
}

if constexpr: La Alternativa Moderna

El if constexpr de C++17 proporciona una forma mas limpia de ramificar en tiempo de compilacion dentro de una sola funcion. La rama descartada no se instancia, por lo que no necesita compilar para el tipo dado. Esto reemplaza en gran medida tag dispatch y SFINAE para casos simples.

if_constexpr.cpp
#include <iostream>
#include <type_traits>
#include <string>
#include <vector>

template <typename T>
std::string stringify(const T& value) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(value);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return '"' + value + '"';
    } else if constexpr (std::is_same_v<T, const char*>) {
        return std::string("\"" ) + value + "\"";
    } else {
        return "[non-printable]";
    }
}

int main() {
    std::cout << stringify(42)              << '\n'; // 42
    std::cout << stringify(3.14)            << '\n'; // 3.140000
    std::cout << stringify(std::string("hello")) << '\n'; // "hello"
    std::cout << stringify("world")         << '\n'; // "world"
}
Buena práctica

Usa esta guia de decision para ramificacion en tiempo de compilacion:

- if constexpr (C++17) — lo mas simple, funciona dentro de una sola funcion, no necesita patrones especiales. Prefierelo para logica basada en tipos directa.
- Concepts/requires (C++20) — lo mejor para restringir sobrecargas de funciones o templates completos. Autodocumentado y da mensajes de error claros.
- Tag dispatch — aun util cuando tienes una jerarquia de tags existente (como categorias de iterador). Las bases de codigo pre-C++17 dependen mucho de esto.
- SFINAE / enable_if — la tecnica mas antigua. Mas dificil de leer, errores cripticos. Evitalo en codigo nuevo si concepts o if constexpr funcionan.
- Especializacion completa — usala para class templates cuando necesitas una implementacion completamente diferente para un tipo especifico.
- Nunca uses especializacion de function templates — usa sobrecarga en su lugar.

Puntos clave
  • La especializacion completa (template <>) proporciona una implementacion personalizada para un tipo especifico
  • La especializacion parcial restringe algunos parametros — disponible solo para class templates, no function templates
  • La especializacion de function templates es propensa a errores — prefiere la sobrecarga o if constexpr
  • Tag dispatch usa tipos struct vacios para seleccionar sobrecargas en tiempo de compilacion mediante iterator_traits
  • if constexpr (C++17) es la alternativa moderna y mas limpia que descarta las ramas no tomadas en tiempo de compilacion

Quiz — Pon a prueba tus conocimientos

(20 XP)

1. Por que deberias evitar especializar function templates?

2. Que tipos de templates soportan especializacion parcial?

3. Que hace `if constexpr` de manera diferente a un `if` regular?