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
Nivel: Intermedio-avanzado (metaprogramación, concepts)
Última actualización: 1 de junio, 2026
1. Anotaciones para reflexión
|
| 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 (reflect, skip 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 meta) resulta 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:
- Que r represente un objeto o variable de tipo U.
- 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.
- 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.
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
- 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
- nlohmann/json – https://github.com/nlohmann/json
- Working Draft Programming Languages - C++ – Metaprogramming library – Value extraction – https://eel.is/c++draft/meta.reflection.extract