Translate

jueves, 17 de abril de 2025

Covarianza y contravarianza en C++


La covarianza y contravarianza en C++ con templates es un poco más manual que en otros lenguajes como Java o C#, porque C++ no tiene soporte directo para la varianza en plantillas. Pero hay formas de simularla o controlarla explícitamente usando herencia, punteros, referencias, std::variant, std::any, SFINAE y concepts.

Primero: ¿Qué es covarianza y contravarianza?

  • Covarianza: cuando un tipo genérico Container<Derived> puede ser tratado como Container<Base>.
  • Contravarianza: lo contrario Container<Base> puede ser usado donde se espera Container<Derived>.
  • Invarianza: no se permite ninguna conversión.


Las Plantillas en C++ son invariantes, esto no es válido:


template <typename T>

class Caja<T> {};


class Animal {};

class Perro : public Animal {};


Caja<Perro>* cp = new Caja<Perro>();

Caja<Animal>* ca = cp;  //  ERROR: no se permite, son tipos distintos


Y listo, fin del post ... Pero pero sí hay covarianza con punteros y referencias a clases (no templates)

Esto funciona:


class Animal {

public:

    virtual void hablar() {}

};


class Perro : public Animal {

public:

    override void hablar() {}

};


Animal* a = new Perro();  // Anda por Covarianza de punteros


¿Cómo resolver o simular varianza con templates?


Usar herencia con punteros o referencias


template <typename T>

class Caja {

public:

    T* valor;

};


Perro* p = new Perro();

Caja<Perro> c1;

c1.valor = p;

Caja<Animal> c2 = c1;  //  No compila directamente

Podemos hacer funciones que trabajen con T* o T& para aprovechar la covarianza de punteros.


Hacer una jerarquía polimórfica manual


class ICajaBase {

public:

    virtual ~ICajaBase() = default;

};


template <typename T>

class Caja : public ICajaBase {

public:

    T valor;

};


Luego podemos trabajar con ICajaBase* y aplicar RTTI o dynamic_cast.


Usar std::variant o std::any para manejar múltiples tipos


#include <variant>

std::variant<Perro, Gato> animal;


Esto evita plantillas covariantes, pero permite flexibilidad en tiempo de ejecución.


Usar std::enable_if, SFINAE o concepts para limitar compatibilidades


template <typename T>

class Caja {

public:

    static_assert(std::is_base_of<Animal, T>::value, "T debe ser un Animal");

};


En C++, las plantillas son invariantes. pero podemos usar covarianza con punteros y referencias a clases base. Si queremos modelar jerarquías con templates, tenemos que hacerlo manualmente usando herencia, punteros, std::variant o SFINAE.