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: 12 de junio, 2026

1. Anotaciones para reflexión. Función std::meta::extract

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) destinada a ser reflexionada. Toma la forma siguiente:
[[ = constant-expression ]]
La anotación asocia a la entidad anotada un valor constante que puede ser inspeccionado mediante reflexión. Dicho valor se representa como un objeto std::meta::info obtenido mediante std::meta::reflect_constant(constant-expression). Esta representación impone ciertas restricciones sobre el tipo T de la expresión constante [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. Un mismo especificador de atributos puede contener un conjunto de atributos o bien un conjunto de anotaciones, pero no una mezcla de ambos (por ejemplo, [[nodiscard, = 12]] es inválido).

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).
El siguiente código ilustrativo añade anotaciones (un int y un double) a la definición de una variable std::stringPosteriormente, un bucle de expansión template for (véase el primer artículo de esta serie para más detalles) inspecciona dichas anotaciones para extraer sus valores e imprimirlos en la salida estándar:

   [[=7, =9.8]] auto str = std::string{}; static_assert(std::meta::annotations_of(^^str).size() == 2); // [7, 9.8] static_assert(std::meta::annotations_of_with_type(^^str, ^^double).size() == 1); // [9.8]    template for (       constexpr std::meta::info annot : std::define_static_array(                                 std::meta::annotations_of(^^str))    ) {       using type = typename[: std::meta::type_of(annot) :];       std::print("{} ", std::meta::extract<type>(annot));    } // imprime: 7 9.8
NOTA 1: FUNCIÓN std::meta::extract
La plantilla de función std::meta::extract se emplea para recuperar de forma tipada la entidad o el valor representado por una reflexión std::meta::info [6]:
template<typename T> consteval auto extract(std::meta::info r) -> T;
Distinguimos tres casos principales según la naturaleza de la reflexión r y el tipo T solicitado:
  • Cuando T es un tipo referencia: si r representa un objeto O, se retorna una referencia a O; en otro caso, se retorna una referencia al objeto declarado por (o al que hace referencia) la variable representada por r.
  • Si r representa una función o un miembro no-estático de clase, el resultado es el puntero a función o el puntero a miembro correspondiente.
  • En el resto de los casos, se devuelve un valor equivalente a static_cast<T>([:R:]), donde R es una expresión constante de tipo std::meta::info tal que R == r es true. Para determinar la viabilidad de la extracción, los tipos se comparan tras eliminarse los cualificadores cv.
Si la extracción solicitada no es válida, la función lanza una excepción std::meta::exception durante la evaluación constante.

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 [7]. 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/c8KoeWaEx.

Introduciremos un pequeño conjunto de anotaciones —reflect, field y skip— propias del framework (que denominaremos mserial, donde la "m" significa meta), a fin de describir la semántica de serialización.

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 datos miembro seleccionados explícitamente para que no participen en el proceso (mediante la anotación skip).
Para ello, definamos la siguiente interfaz general para la serialización y deserialización de objetos mediante reflexión, la cual contiene los mencionados tipos de anotaciones reflect, field y skip, así como una serie de conceptos y funciones consteval auxiliares que explicaremos más adelante:

namespace mserial {    inline constexpr struct { } reflect{}; // permitir reflexión (opt-in)    inline constexpr struct { } skip{};    // ignorar un campo   struct field { // dar un nombre personalizado a un campo       char const* name;     template<std::size_t N>       consteval 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() -> std::ranges::view auto; template<std::meta::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

Observemos que los valores de las anotaciones deben poder usarse como argumentos de plantilla constantes. Los literales de cadena no cumplen esta condición, lo que justifica el empleo de std::define_static_string para dotar de carácter estructural a los nuevos nombres de claves introducidos mediante anotaciones field.
Cabe destacar que el esquema de anotaciones mserial resulta independiente del formato (como JSON o XML), limitándose a describir la semántica de serialización.
Como ejemplo concreto, 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 —debido, por ejemplo, a reglas de estilo que el programador deba aplicar consistentemente—, pero no así los nombres que se desea 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 forma directa de objetos Gamer (clase convenientemente anotada con [[=mserial::reflect]]), empleando las claves alternativas facilitadas para los datos miembro name_ y age_ e ignorando password_:

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

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 instanciadas de forma automática con ayuda del 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

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í los tipos de anotación 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 non_ignorable_data_members 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:

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

El uso de access_context::unchecked() permite aquí que la política de serialización se defina de forma independiente del control de acceso de los miembros.

A partir del valor de reflexión de un dato miembro no-estático dm perteneciente a una clase reflectable, la siguiente función field_name devuelve un std::string_view al 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 (stdm::is_nonstatic_data_member(dm) and reflectable<typename[:stdm::parent_of(dm):]>) consteval auto field_name() -> std::string_view { auto name = [](stdm::info annot) -> std::string_view {       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 name(fa); })          .value_or(stdm::identifier_of(dm)); } } // namespace mserial

Toleramos duplicados idénticos en anotaciones mserial::field por idempotencia y por el interés intrínseco del código resultante, pero podríamos optar naturalmente por detener la compilación si un dato miembro poseyera más de una anotación de ese tipo, contuviesen o no el mismo nombre alternativo. Para ello, utilizaríamos la condición field_annot and (*field_annot != annot) en la sentencia if.

En último lugar, procedemos a definir  las funciones from_json y to_json encargadas de la serialización y deserialización por reflexión entre objetos nlohmann::json y tipos T anotados con [[=mserial::reflect]]. Estas funciones deben quedar definidas en el mismo espacio de nombres (que puede ser el espacio de nombres global) en que se declaren los tipos a serializar:

template<typename 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<typename 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 en la Sección 2.
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 en 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) se deriva de forma directa, y de manera específica para cada tipo, a partir de la información suministrada por el 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. Working Draft Programming Languages - C++ – Metaprogramming library – Value extraction – https://eel.is/c++draft/meta.reflection.extract
  7. nlohmann/json – https://github.com/nlohmann/json

Comentarios