Reflexión en C++26 (Parte 2)

Lecturas de la serie
  • Parte 1: Operador de reflexión ^^ y splicers [:...:]. Sentencias de expansión (template for). [Enlace]
  • Parte 2: Anotaciones para reflexión. Extracción (extract).
  • Parte 3: Generación de agregados (define_aggregate). Bloques consteval.  [Por publicar]
  • Parte 4: Sustitución (substitute). [Por publicar]
Sobre este artículo
     Tiempo estimado de lectura: 10 minutos
     Nivel: Intermedio-avanzado (metaprogramación, concepts)
     Última actualización: 1 de junio, 2026

1. Anotaciones para reflexión

Imagen generada con inteligencia artificial
Imagen generada con inteligencia artificial para fines divulgativos
Como parte de sus nuevas funcionalidades de reflexión estática, C++26 introduce la capacidad de anotar declaraciones de manera que puedan ser observadas mediante reflexión en tiempo de compilación [1].

Una anotación (annotation) es una construcción sintácticamente similar a un atributo cuyo contenido es una expresión constante (constant-expression). Su sintaxis toma la forma:
[[ = constant-expression ]]
La anotación asocia a la entidad anotada un meta-valor que puede ser inspeccionado mediante reflexión. Para ello, la expresión constant-expression se transforma en un valor de reflexión std::meta::info mediante std::meta::reflect_constant(constant-expression). Esta transformación impone ciertas restricciones sobre el tipo T de dicha expresión [2]:
  • std::is_copy_constructible<T> debe ser true.
  • T debe ser un tipo estructural [3] sin cualificadores cv y no puede ser un tipo referencia.
Podemos aplicar una anotación a cualquier declaración de un tipo, alias de tipo, variable, función, parámetro de función de tipo no-void, espacio de nombres, enumerador, especificador de clase base o dato miembro no-estático [4]. Las anotaciones son siempre distintas entre sí, incluso si tienen valores equivalentes y se aplican a la misma declaración.

La biblioteca de reflexión (std::meta) proporciona funciones consteval auxiliares que operan sobre valores de reflexión std::meta::info y que permiten recuperar las anotaciones de una entidad reflejada item en forma de std::vector<std::meta::info>. Distinguimos [5]:
  • annotations_of(item): devuelve todas las anotaciones de item.
  • annotations_of_with_type(item, type): devuelve todas las anotaciones a de item tales que remove_const(type_of(a)) == remove_const(type).

2. Un caso de uso: serialización JSON

A modo de ejemplo motivador, analizaremos la implementación de un framework sencillo para la conversión bidireccional entre objetos C++ y valores JSON mediante reflexión estática, empleando la biblioteca "JSON for Modern C++" de Niels Lohmann [6]. Nuestra implementación se inspirará en el ejemplo 3.3. (Serialization) de la referencia bibliográfica [1]. Puede consultarse el código compilado con GCC 16.1 en el siguiente enlace de Compiler Explorer: https://godbolt.org/z/jqh1znWT3.

En concreto, buscamos que una clase pueda optar explícitamente por la inspección mediante reflexión de sus datos miembro no-estáticos —incluidos aquellos que no formen parte de la interfaz pública— para procesos de serialización y deserialización. A tal efecto, definiremos la anotación reflect. Asimismo, permitiremos:
  • Especificar nombres personalizados alternativos para los campos cuando éstos no deban coincidir con los identificadores originales de los datos miembro (mediante anotaciones field).
  • Ignorar aquellos atributos seleccionados explícitamente para que no participen en el proceso (mediante la anotación skip).
Para ello, introduciremos la siguiente interfaz general para la serialización y deserialización de objetos mediante reflexión, la cual introduce el conjunto reducido de anotaciones indicadas (reflectskip y field), así como una serie de conceptos y funciones consteval auxiliares que explicaremos más adelante:

namespace mserial {    inline struct { } reflect{}; // permitir reflexión (opt-in)    inline struct { } skip{};    // ignorar un campo   struct field { // dar un nombre personalizado a un campo       char const* name;     template<std::size_t N>       constexpr field(char const (&str)[N])          : name{std::define_static_string(str)}     { }    }; template<typename T> concept reflectable = /* ... */; template<reflectable T> consteval auto non_ignorable_data_members() -> stdr::view auto; template<stdm::info dm> requires (std::meta::is_nonstatic_data_member(dm) and reflectable<typename[:std::meta::parent_of(dm):]>) consteval auto field_name() -> std::string_view; } // namespace mserial
Cabe destacar que el esquema de anotaciones mserial (donde la "m" significa metaresulta independiente del formato (como JSON o XML), limitándose a describir la semántica de serialización.
Por ejemplo, consideremos la clase:


class [[=mserial::reflect]] Gamer { private:    [[=mserial::field{"name"}]]     std::string name_;    [[=mserial::field{"age"}]]    int age_ = 18;    [[=mserial::skip]]    std::string password_; public:     Gamer() = default; Gamer(std::string_view name, int age, std::string_view password)       : name_{name}, age_{age}, password_{password} { }    auto const& name() const { return name_; } auto age() const { return age_; } // resto de la interfaz pública... };  
Obsérvese cómo los identificadores de los datos miembro privados de la clase Gamer terminan en guion bajo, pero no así los nombres que deseamos emplear para su serialización y deserialización, señalados en las anotaciones [[=mserial::field{new_name}]].
Nuestro framework permitirá la serialización y deserialización de objetos Gamer (clase convenientemente anotada con [[=mserial::reflect]]) de forma directa, ignorando el atributo password_:

   auto const j1 = nlohmann::json{{"name", "Ian Malcolm"}, {"age", 37}};    auto const p1 = j1.get<Gamer>(); // deserialización (invoca from_json)    std::println("name: {} | age: {}", p1.name(), p1.age());    // imprime: name: Ian Malcolm | age: 38    // ------------------------------------------------------------------    auto const p2 = Gamer{"Sarah Connor", 29, "skynet_is_the_enemy"};    auto const j2 = nlohmann::json(p2); // serialización (invoca to_json)    std::println("name: {} | age: {}",                 j2["name"].get<std::string>(), j2["age"].get<int>());    // imprime: name: Sarah Connor | age: 45

Nuestro objetivo es que las funciones to_json y from_json requeridas por la biblioteca nlohmann::json —encargadas de la serialización y deserialización entre objetos Gamer y nlohmann::json— sean generadas de forma automática por el sistema de reflexión. Como ya hemos indicado, se ignorarán los campos anotados con [[=mserial::skip]] y se emplearán las claves alternativas introducidas mediante [[=mserial::field{new_name}]].

3. Implementación. Función std::meta::extract

Importemos, en primer lugar, las bibliotecas relevantes y definamos una serie de alias convenientes para el resto de nuestro código:

  import std;    import nlohmann.json;    namespace stdm = std::meta;    namespace stdr = std::ranges;    namespace stdv = std::views;

A continuación, definamos un concepto mserial::reflectable que identifique a las clases que, de forma explícita, opten por permitir la inspección de sus datos miembro mediante reflexión (es decir, cuyas declaraciones sean anotadas con [[=mserial::reflect]]):

namespace mserial { // insertar aquí las anotaciones reflect, skip y field template<typename T> concept reflectable = stdm::is_class_type(^^T) and not stdm::annotations_of_with_type( ^^T, ^^decltype(mserial::reflect) ).empty(); // namespace mserial continúa...

La siguiente función obtiene, para una clase T que cumpla el concepto mserial::reflectable, el rango de sus datos miembro no-estáticos —ya sean públicos, privados o protegidos— que no estén anotados con [[=mserial::skip]] y que, por tanto, deban participar en la serialización:

template<reflectable T> consteval auto non_ignorable_data_members() -> stdr::view auto {    auto without_skip_annotation = [](stdm::info data_member) -> bool {       return stdm::annotations_of_with_type(          data_member,          ^^decltype(mserial::skip)       ).empty();    };     return stdm::nonstatic_data_members_of(^^T, stdm::access_context::unchecked())         | stdv::filter(without_skip_annotation); } // namespace mserial continúa...

A partir del valor de reflexión de un dato miembro no-estático dm perteneciente a una clase reflectable,  la siguiente función devuelve un std::string_view con el nombre indicado en la anotación [[=mserial::field{new_name}]]. Si no existe dicha anotación, se retorna el identificador original. Finalmente, si el mismo miembro presenta varias anotaciones field con nombres distintos, la función emite una excepción de reflexión std::meta::exception:

template<stdm::info dm> requires (std::meta::is_nonstatic_data_member(dm) and reflectable<typename[:stdm::parent_of(dm):]>) consteval auto field_name() -> std::string_view {    auto name = [](stdm::info annot) -> char const* {       return stdm::extract<mserial::field>(annot).name;    };    auto field_annot = std::optional<stdm::info>{};    template for (       constexpr stdm::info annot : std::define_static_array(                   stdm::annotations_of_with_type(                     dm, ^^mserial::field))    ) {       if (field_annot and (name(*field_annot) != name(annot))) {           throw stdm::exception{             "Different 'field' annotations found for\ the same data member", dm};       }       field_annot = annot;     }    return field_annot          .transform([&name](stdm::info fa){ return std::string_view{name(fa)}; })          .value_or(stdm::identifier_of(dm)); } } // namespace mserial
NOTA 1: FUNCIÓN std::meta::extract
La función genérica std::meta::extract se emplea para extraer un valor a partir de una reflexión std::meta::info si se conoce su tipo, devolviendo una referencia al objeto o variable representada [7]:
template<typename T> consteval auto extract(std::meta::info r) -> T;
Aquí, T es un tipo referencia. El proceso requiere:
  1. Que r represente un objeto o variable de tipo U.
  2. Que el tipo solicitado sólo difiera del original en cualificaciones de tipo (como const): is_convertible_v<remove_reference_t<U>(*)[],​ remove_reference_t<​T>(​*)[]> es true.
  3. De representar r a una variable, ésta es utilizable en expresiones constantes o ha comenzado su ciclo de vida dentro de la propia expresión constante bajo evaluación.
Si no se cumplen estas condiciones, se lanza una excepción std::meta::exception.
En último lugar, procedemos a definir las funciones encargadas de la serialización/deserialización por reflexión entre objetos nlohmann::json y cualquier tipo T construible por defecto y anotado con [[=mserial::reflect]]:

template<std::default_initializable T> void from_json(nlohmann::json const& j, T& obj) {    static_assert(mserial::reflectable<T>,       "type T must be annotated with [[=mserial::reflect]]");     template for (       constexpr auto dm : std::define_static_array( mserial::non_ignorable_data_members<T>())    ) {       constexpr auto field = mserial::field_name<dm>();       j.at(field).get_to(obj.[:dm:]);     } } template<std::default_initializable T> void to_json(nlohmann::json& j, T const& obj) {    static_assert(mserial::reflectable<T>,        "type T must be annotated with [[=mserial::reflect]]");     template for (       constexpr auto dm : std::define_static_array( mserial::non_ignorable_data_members<T>())    ) {       constexpr auto field = mserial::field_name<dm>();       j[field] = obj.[:dm:];     } }

donde hemos optado por emplear aserciones static_assert en lugar de expresiones requires a fin de proporcionar diagnósticos de compilación más claros.

Con estas definiciones, la clase Gamer satisface automáticamente la interfaz esperada por la biblioteca nlohmann::json, permitiendo su serialización y deserialización en la forma presentada al inicio de esta sección.
Observemos que, frente a los mecanismos tradicionales basados en macros o funciones intrusivas, el enfoque adoptado en este ejemplo permite describir la semántica de serialización directamente sobre la definición del tipo Gamer a través de anotaciones. La lógica de la serialización/deserialización (funciones to_json y from_json) queda generada de forma directa y específica a través del sistema de reflexión.

Referencias bibliográficas

  1. P3394R4 – Annotations for Reflection – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3394r4.html
  2. cppreference – std::meta::reflect_constant – https://en.cppreference.com/cpp/meta/reflect_constant
  3. cppreference - Template Parameters – https://en.cppreference.com/w/cpp/language/template_parameters.html
  4. Working Draft Programming Languages - C++ – Metaprogramming library –  Attributes – Annotations – https://eel.is/c%2B%2Bdraft/dcl.attr.annotation
  5. Working Draft Programming Languages - C++ – Metaprogramming library – Annotation reflection –  https://eel.is/c++draft/meta.reflection#annotation
  6. nlohmann/json – https://github.com/nlohmann/json
  7. Working Draft Programming Languages - C++ – Metaprogramming library – Value extraction – https://eel.is/c++draft/meta.reflection.extract