std::variant - Polimorfismo sin herencia

Introducción


Última actualización (terminología y funcionalidades de C++23): 18 de enero de 2026

Como analizamos en un post anterior, la plantilla de clase std::variant<>, incluida por vez primera en el estándar C++17, proporciona una unión etiquetada (tipo suma) con la funcionalidad típica de las uniones de C, si bien garantizando la seguridad de tipos.

Así, una instancia de std::variant<> podrá contener, en un instante dado, un objeto perteneciente a uno de varios tipos alternativos permitidos, o ningún valor en caso de error.


Un nuevo tipo de polimorfismo


std::variant<> da pie a un nuevo mecanismo de polimorfismo que no requiere el empleo de las tradicionales jerarquías de clases basadas en la herencia, siendo especialmente indicado en aquellos casos en que:
  • El conjunto de tipos sea fijo y cerrado.
  • No resulte imprescindible emplear herencia y convenga realizar dispatch sin coste virtual.
  • Se desee adoptar un enfoque funcional y recuperar la semántica de valor.
En efecto, imaginemos un videojuego que, atendiendo al nivel de dificultad seleccionado en tiempo de ejecución, enfrenta al jugador a una especie distinta de dinosaurio:

struct Brachio {     double height = 12.0; }; struct Raptor {     double speed = 70.0; }; struct Trex {     bool is_in_attack = true; };

Observemos, pues esto resultará crucial en nuestra discusión posterior, que ninguna de estas clases hereda de un tipo base, así como que no es necesario que compartan una interfaz común: la clave residirá en cómo las acciones externas serán gestionadas para cada tipo concreto mediante std::visit.

Definamos un alias para una especialización de std::variant<> que pueda contener objetos de las tres especies anteriores:

using Dinosaur = std::variant<Brachio, Raptor, Trex>;

Empleemos, a continuación, la función genérica match() implementada en un post anterior, gracias a la cual podemos discernir en tiempo de ejecución el tipo estático de dato contenido en std::variant<> y aplicar, para éste, la sobrecarga de función adecuada de entre un conjunto proporcionado de handlers (típicamente lambdas):

template<typename V> concept Variant = requires(V&& v) { std::visit([](auto&&){}, std::forward<V>(v)); };   template<Variant V>   [[nodiscard]] constexpr auto match(V&& var)   {      return [t = std::tuple<V>{std::forward<V>(var)}]             <typename... Handlers>(Handlers&&... handlers) mutable -> decltype(auto) {                struct Overload : Handlers... { using Handlers::operator()...; };                return std::visit(Overload{std::forward<Handlers>(handlers)...},   std::forward<V>(std::get<0>(t)));             };   }

La ejecución de la siguiente función principal main() muestra cómo, dependiendo del nivel de dificultad (entero level) seleccionado por el usuario en tiempo de ejecución, el variant enemy se puebla con una especie de dinosaurio distinta (Brachio para nivel 0Raptor para nivel 1 y Trex para nivel 2). La función match() invocada por print_species() se encarga entonces de mostrar en pantalla el mensaje que corresponda a cada dinosaurio. Observemos, en particular, que en el caso del T-Rex el mensaje se encuentra especializado con el fin de explicitar su estatus especial como final boss:

#include <cassert> #include <concepts> #include <exception> #include <iostream> #include <print> #include <string> #include <utility> #include <variant> // sitúa aquí las estructuras de dinosaurios, el alias Dinosaur y // el código de implementación de la función match() auto create_dino(int level) -> Dinosaur // factoría { assert(level>=0 and level<=2); // precondición     auto dino = Dinosaur{};     switch (level) {        case 0: dino = Brachio{}; break;        case 1: dino = Raptor{}; break;        case 2: dino = Trex{}; break;        default: std::abort();     }     return dino; } void print_species(Dinosaur const& dino) {     match(dino)(        [](Brachio const& brc){ // mensaje específico para Brachio            std::println("Enemy: Brachio - height: {}m", brc.height);        },        [](Raptor const& rpt){ // mensaje específico para Raptor            std::println("Enemy: Raptor - speed: {}km/h", rpt.speed);        },        [](Trex const& trx){ // mensaje específico para Trex            std::println("Final boss! Trex - Is in attack? {}", trx.is_in_attack);        }     ); } auto main() -> int {     auto level = -1;     while (level < 0 or level > 2) {     std::print("Select level [0,1,2]: ");     std::cin >> level; }     auto const enemy = create_dino(level);     print_species(enemy); }

Como ya hemos mencionado, el polimorfismo proporcionado por std::variant<> puede resultar la opción más adecuada cuando nuestro conjunto de clases sea cerrado. De forma notable, evitamos la necesidad de heredar de una clase base (una técnica invasiva), así como el uso de métodos virtuales y el alojamiento dinámico. Se recupera, además, una semántica de valor frente a una de referencia. Esto último puede resultar especialmente eficiente cuando situemos objetos de tamaño similar (como los dinosaurios de nuestro ejemplo) en un array, pues éstos quedarán localizados de forma contigua en memoria. Hablamos, aquí, de un contenedor heterogéneo como el del siguiente ejemplo:

   auto dinos = std::vector<Dinosaur>{};    dinos.push_back(Trex{});    dinos.push_back(Brachio{});    dinos.push_back(Raptor{});    for (auto const& dino : dinos) {       print_species(dino);    }


Referencias bibliográficas:
  • Nikolai Wuttke - Meeting C++ 2018 - std::variant and the power of pattern matching - https://www.youtube.com/watch?v=CELWr9roNno
  • Nicolai Josuttis - Meeting C++ 2019 - Combining C++17 Features - https://www.youtube.com/watch?v=6HoxXeEBtW0

No hay comentarios:

Publicar un comentario