Nivel: Avanzado (C++26, metaprogramación, reflexión estática)
Esta nueva meta-programación reflexiva se basa en:
- La representación de elementos del programa mediante expresiones constantes (constant‑expressions) que producen valores de reflexión —reflexiones— del tipo opaco std::meta::info.
- El operador unario de reflexión (prefijo ^^), capaz de proyectar múltiples construcciones gramaticales del lenguaje —como namespace-names, type-ids, id-expressions o el espacio de nombres global ::— a sus correspondientes reflexiones.
- Un amplio conjunto de operaciones consteval en la cabecera <meta> para trabajar con dichas reflexiones (incluyendo la derivación de otras reflexiones).
- Los denominados splicers [:…:] (más formalmente, splice-expressions), que permiten generar construcciones sintácticas a partir de reflexiones.
// podemos obtener un std::string_view (almacenamiento estático) con el nombre del tipo: constexpr auto str = std::string_view{std::meta::display_string_of(refl)}; std::println("{}", str); // imprime: int // usamos el splicer [:refl:] para inyectar el tipo representado en código real: auto const i = typename[:refl:]{7}; // equivalente a: auto const i = int{7}; std::println("{} bytes", sizeof(typename[:refl:])); // imprime típicamente: 4 bytes
Iremos explorando los aspectos de la reflexión en C++ a través de varios ejemplos de nivel relativamente avanzado. En este artículo, en particular, abordaremos un ejemplo canónico: el diseño de un parseador de opciones de línea de comandos (Command‑Line Options) que mapee automáticamente valores a datos miembro de un agregado. Puede consultarse una codificación simplificada en el ejemplo 3.11 de la referencia [1].
El código puede compilarse con GCC (rama trunk), habilitando el soporte experimental de reflexión mediante -freflection y utilizando -std=c++26.
Ejemplo 1: Parseador de opciones de línea de comandos (CLI parser)
--option_name option_value
Cada opción deberá comenzar necesariamente por dos guiones (--), seguidos del nombre de la opción option_name. Su valor asociado (opcional en el caso de booleanos) se suministrará como un token independiente option_value. Por diseño, no se admitirán valores embebidos mediante el operador = (del tipo --option_name=option_value) ni abreviaturas de opción con un solo guion (como -o). Únicamente las opciones booleanas podrán aparecer sin valor explícito, en cuyo caso se inferirá implícitamente como true.
El analizador no admitirá argumentos posicionales: cualquier token que no comience por -- y que no sea consumido explícitamente como valor de una opción será considerado inválido. Tras procesar todas las opciones reconocidas, de permanecer tokens sin consumir (incluidos los posicionales o cualquier otro valor ajeno a nuestro modelo de opciones) el analizador producirá un error indicando que existen argumentos sobrantes (leftovers).
Como primer paso, desglosaremos las cabeceras estándar necesarias para nuestro parseador:
- El nombre del dato miembro (std::string_view sobre una secuencia de caracteres constante, gestionada por el compilador de forma estática).
- Un variante (std::variant) capaz de almacenar el puntero al dato miembro. Dado que distintos tipos de datos miembro conducen a tipos diferentes de punteros, es necesario un tipo suma que permita agruparlos en un único array homogéneo.
using Variant = std::variant<decltype(&[:nsdms[I]:])...>; // (C) return std::array{ // (D) Nsdm_info<decltype(&[:nsdms[I]:])...>{ .identifier = stdm::identifier_of(nsdms[I]), .member_ptr = Variant{std::in_place_index<I>, &[:nsdms[I]:]} // (E) }... }; }(std::make_index_sequence<nsdms.size()>{}); }
- (A): Obtenemos en tiempo de compilación todos los datos miembro no-estáticos declarados directamente en T (no heredados). El operador ^^T genera un meta-valor std::meta::info que representa al tipo T. A partir de él, nonstatic_data_members_of() obtiene una secuencia de meta-valores std::meta::info, uno por cada dato miembro no estático de T. Finalmente, define_static_array() materializa esa secuencia en un span indexable, lo que nos permite acceder al meta-valor I‑ésimo —aquél que representa al miembro I‑ésimo de T— mediante nsdms[I].
- (B): La colección obtenida en (A) se despliega con std::index_sequence<I...>, que genera un pack de índices I de 0 a N-1, siendo N el número de datos miembro accesibles de T (véanse más detalles acerca del contexto de acceso más adelante).
- (C): Definimos un tipo Variant con una alternativa por cada dato miembro de T. Cada alternativa es de tipo M T::*, siendo M el tipo del dato miembro. La declaración decltype(&[:nsdms[I]:]) nos permite determinar en tiempo de compilación el tipo del puntero al dato miembro I-ésimo.
- (D): Generamos finalmente un std::array de objetos Nsdm_info y longitud N=sizeof...(I), que será empleado posteriormente como tabla de despacho. Para cada dato miembro:
- identifier almacena el nombre del miembro (identifier_of), y
- member_ptr almacena el puntero al miembro, convenientemente empaquetado en el variante.
- (E): Es importante notar que varias alternativas del variante pueden tener el mismo tipo (por ejemplo, si T contiene varios int, las correspondientes alternativas son todas de tipo int T::*). Para identificar cada miembro de forma única, el variante se inicializa mediante std::in_place_index<I> [3], asignando a cada entrada el índice correspondiente a su orden de declaración. La expresión &[:nsdms[I]:] produce el puntero al miembro I‑ésimo, que queda almacenado sin ambigüedad en la alternativa I-ésima.
- access_context::current: representa el contexto de acceso actual del punto en el que se realiza la reflexión, respetando íntegramente las reglas habituales de acceso en C++ (incluyendo public, protected, private, herencia y amistad). Sólo permite reflexionar miembros que sean accesibles en ese punto del programa.
- access_context::unprivileged: equivale a evaluar la reflexión desde el espacio de nombres global, sin privilegios de clase ni amistad. Ello implica en la práctica que el acceso quede limitado a la interfaz pública.
- access_context::unchecked: omite las comprobaciones de control de acceso durante la reflexión y permite reflexionar todos los miembros del tipo, incluidos private y protected, sin aplicar las restricciones habituales de acceso.
- access_context::via(cls): mantiene el ámbito actual pero establece cls como clase designante para efectos de control de acceso. Sirve para simular acceso como si se estuviese dentro de otra clase.
template<typename T> concept cli_parsable = std::is_default_constructible_v<T> and (std::same_as<T, bool> or std::same_as<T, std::string> or std::is_arithmetic_v<T> or istream_extractable<T>);
- el nombre de una opción (string_view name), y
- el valor textual asociado (string_view value, por ejemplo "4", "true", "5.7", "[2,7]", etc.).
enum class Parse_errc { missing, invalid };
template<cli_parsable T> [[nodiscard]] auto parse_cli_value(std::string_view name, std::string_view value) -> std::expected<T, std::string> { using U = std::remove_cvref_t<T>; auto err = [&](Parse_errc ec) { return std::unexpected(std::format("{} value for --{} (expected {})", (ec == Parse_errc::missing)? "missing" : "invalid", name, stdm::display_string_of(^^U))); }; if constexpr (std::same_as<U, bool>) { if (value.empty() or equals_ascii(value, "true") or equals_ascii(value, "1") or equals_ascii(value, "yes") or equals_ascii(value, "y")) { return true; } if (equals_ascii(value, "false") or equals_ascii(value, "0") or equals_ascii(value, "no") or equals_ascii(value, "n")) { return false; } return err(Parse_errc::invalid); } else if constexpr (std::same_as<U, std::string>) { if (value.empty()) { return err(Parse_errc::missing); } return std::string{value}; } else if constexpr (std::is_arithmetic_v<U>) { if (value.empty()) { return err(Parse_errc::missing); } auto res = U{}; auto const first = value.data(); auto const last = first + value.size(); auto const [ptr, ec] = std::from_chars(first, last, res); if (ec != std::errc{} or ptr != last) { return err(Parse_errc::invalid); } return res; } else if constexpr (istream_extractable<U>) { if (value.empty()) { return err(Parse_errc::missing); } auto res = U{}; auto iss = std::istringstream{value}; iss.imbue(std::locale::classic()); if (not (iss >> res)) { return err(Parse_errc::invalid); } iss >> std::ws; if (not iss.eof()) { return err(Parse_errc::invalid); } return res; } else { std::unreachable(); } }
template<typename Member, typename Class> struct Data_member<Member Class::*> { using type = Member; };
template<typename T> using Data_member_t = typename Data_member<T>::type; template<typename Opts> // Opts debe ser un agregado sin clases base requires std::is_aggregate_v<Opts> and (stdm::bases_of(^^Opts, stdm::access_context::current()).empty()) [[nodiscard]] auto parse_cli_options(char** argv, std::size_t argc) -> std::expected<Opts, std::string> { auto res = Opts{}; auto const args = std::vector<std::string_view>{argv + 1, argv + argc}; auto is_name = [&](std::size_t idx) { return args[idx].starts_with("--"); }; auto is_followed_by_value = [&](std::size_t idx) { return idx + 1 < args.size() and not is_name(idx + 1); }; constexpr auto table = nsdm_table<Opts>(); // datos miembro no-estáticos de Opts auto consumed = std::vector<bool>(args.size(), false); for (auto i = 0uz; i < args.size(); /* incremento dentro */) { if (not is_name(i)) { ++i; continue; } auto const arg = args[i]; // "--option_name" auto const option_name = arg.substr(2); // "option_name" // buscamos el miembro de Opts cuyo identificador coincida con el nombre de la opción: if ( auto const it = stdr::find_if(table, [&](auto&& m){ return m.identifier == option_name; }); it != table.end() ){ auto const r = it->member_ptr.visit( [&](auto const& mptr) -> std::expected<void, std::string> { using M = Data_member_t<std::remove_cvref_t<decltype(mptr)>>; static_assert(not std::is_const_v<M>, "cannot assign to const member"); static_assert(not std::is_array_v<M>, "cannot assign to array member"); static_assert(cli_parsable<M>, "unsupported CLI option type"); auto const has_value = is_followed_by_value(i); auto const option_value = has_value? args[i + 1] : std::string_view{}; if (auto p = parse_cli_value<M>(option_name, option_value)) { res.*mptr = std::move(*p); consumed[i] = true; if (has_value) { consumed[i + 1] = true; } i += has_value? 2 : 1; return {}; } else { return std::unexpected{p.error()}; } } ); if (not r) { return std::unexpected{r.error()}; } } else { return std::unexpected{std::format("unknown CLI option --{}", option_name)}; } } // comprobamos si existen tokens sin procesar: auto leftovers = std::string{}; for (auto const i : stdv::indices(args.size())) { if (not consumed[i]) { if (not leftovers.empty()) { leftovers += ", "; } leftovers += args[i]; } } if (not leftovers.empty()) { return std::unexpected{std::format("unused CLI tokens remained: [{}]", leftovers)}; } return res; }
auto main(int argc, char* argv[]) -> int { auto const c = parse_cli_options<Circle>(argv, argc); if (not c) { std::println("Error: {}", c.error()); return EXIT_FAILURE; }
auto const& [name, radius, borderw, opacity, color, filled] = *c; auto const& [r, g, b] = color; std::println("name = {}", name); std::println("radius = {}", radius); std::println("border width = {}", borderw); std::println("opacity = {}", opacity); std::println("color = {},{},{}", r, g, b); std::println("filled = {}", filled); return EXIT_SUCCESS; }
--name "Circle #1" --radius 9.4 --border_width 2 --opacity 0.8 --color 44,55,125 --filledname = Circle #1
radius = 9.4
border width = 2
opacity = 0.8
color = 44,55,125
filled = true--name "Circle #1" --radius ra --border_width 2 --opacity 0.8 --color 44,55,125 --filledError: invalid value for --radius (expected U {aka double})--name "Circle #1" --radius --border_width 2 --opacity 0.8 --color 44,55,125 --filledError: missing value for --radius (expected U {aka double})
--name "Circle #1" --r 9Error: unknown CLI option --ri --name "Circle #1" 0.5 --radius 9.4 jError: unused CLI tokens remained: [i, 0.5, j]- P2996R13 - Reflection for C++26 - https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html
- Working Draft Programming Languages - C++ - Metaprogramming library - https://eel.is/c++draft/meta.reflection
- cppreference - std::in_place - https://en.cppreference.com/w/cpp/utility/in_place.html
- cppreference - std::expected - https://en.cppreference.com/w/cpp/utility/expected.html


No hay comentarios:
Publicar un comentario