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
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 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::string. Posteriormente, 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.
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
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
- P3394R4 – Annotations for Reflection – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3394r4.html
- cppreference – std::meta::reflect_constant – https://en.cppreference.com/cpp/meta/reflect_constant
- cppreference - Template Parameters – https://en.cppreference.com/w/cpp/language/template_parameters.html
- Working Draft Programming Languages - C++ – Metaprogramming library – Attributes – Annotations – https://eel.is/c%2B%2Bdraft/dcl.attr.annotation
- Working Draft Programming Languages - C++ – Metaprogramming library – Annotation reflection – https://eel.is/c++draft/meta.reflection#annotation
- Working Draft Programming Languages - C++ – Metaprogramming library – Value extraction – https://eel.is/c++draft/meta.reflection.extract
- nlohmann/json – https://github.com/nlohmann/json
Comentarios
Publicar un comentario