Lecturas de la serie
- Parte 1: Operador de reflexión ^^ y splicers [:...:]. Ejemplo: parseador de opciones de línea de comandos.
- Parte 2: Sentencias de expansión (template for). [Por publicar]
- Parte 3: Generación de agregados (define_aggregate). [Por publicar]
- Parte 4: Anotaciones para reflexión. [Por publicar]
- Parte 5: Sustitución (substitute). [Por publicar]
Sobre este artículo
Tiempo estimado de lectura: 30 minutos
Nivel: Intermedio-avanzado (metaprogramación, constexpr/consteval, std::expected)
Última actualización: 15 de marzo, 2026
1. Introducción: operador de reflexión ^^ y splicers [:…:]
Esta serie de artículos busca introducir al lector en las nuevas capacidades de reflexión estática que formarán parte del próximo estándar ISO C++26, tal y como fueron introducidas en la propuesta P2996 [1] y que ya han sido incorporadas (con algunas modificaciones y extensiones [2-7]) al draft del estándar [8]. Por reflexión estática entendemos aquí la capacidad de un programa de analizar su propia estructura durante la compilación —a diferencia de la reflexión en tiempo de ejecución de lenguajes como Java o Python— y de generar código derivado de dicha información.
Esta nueva funcionalidad constituye uno de los avances más significativos en la evolución reciente del lenguaje, al ampliar de manera sustancial las capacidades de meta-programación disponibles para el desarrollador. Se fundamenta en los siguientes principios:
- 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 a sus correspondientes reflexiones. Se contemplan aquí namespace-names, type-ids, id-expressions (variables, funciones, datos miembro estáticos y no estáticos, plantillas y miembros de plantillas, enumeradores) y el espacio de nombres global ::. El operador produce prvalues de tipo std::meta::info.
- 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 [: refl :], que permiten generar elementos gramaticales a partir de reflexiones (donde refl debe ser una expresión constante de tipo std::meta::info).
Podemos entender este sistema como una interacción entre dos dominios: el dominio del programa (tipos, miembros, expresiones, etc.) y el meta‑dominio formado por los valores std::meta::info. El operador ^^ proyecta elementos del programa al meta‑dominio, donde pueden inspeccionarse y manipularse (por ejemplo, consultando sus tipos, sus identificadores, sus miembros o sus argumentos de plantilla). Los splicers realizan el movimiento inverso, reintroduciendo (reificando) en el código construcciones derivadas de esas reflexiones. Por supuesto, un splicer no puede expandirse a código arbitrario: siempre deberá producir una construcción bien formada, válida en el contexto gramatical donde aparezca. Esta bidireccionalidad constituye la base de la reflexión estática en C++26.
A modo de ejemplos iniciales:
// EJEMPLO 1: Reflexión de un tipo
// producimos un meta-valor std::meta::info que representa al tipo int:
constexpr auto refl = ^^int;
// realizamos algunas verificaciones con las siguientes funciones consteval:
// · consteval auto is_arithmetic_type(std::meta::info type) -> bool
// · consteval auto alignment_of(std::meta::info r) -> std::size_t
static_assert(std::meta::is_arithmetic_type(refl)); // ok, int es tipo aritmético
static_assert(std::meta::alignment_of(refl) == 4); // ok, int tiene alineamiento típico de 4 bytes
// obtenemos el nombre del tipo mediante la función
// consteval auto display_string_of(std::meta::info r) -> std::string_view:
constexpr auto type_name = std::meta::display_string_of(refl);
static_assert(type_name == "int"); // ok
// el splicer [:refl:] reinyecta el tipo representado dentro del código:
using T = typename[:refl:]; // el compilador lo traduce a: using T = int;
static_assert(sizeof(T) == 4); // ok, sizeof(int) es típicamente 4 bytes
auto n = T{1}; // el compilador lo traduce a: auto n = int{1};
// EJEMPLO 2: Reflexión de datos miembro no-estáticos de una clase
struct S {
int x{};
double y{};
};
// obtenemos en tiempo de compilación un span de valores std::meta::info que
// representan los datos miembros no-estáticos de S, en el mismo orden de declaración:
constexpr auto nsdms = std::define_static_array(
std::meta::nonstatic_data_members_of(^^S, std::meta::access_context::current())
);
static_assert(nsdms.size() == 2); // ok, se han reflexionado dos datos miembro no-estáticos
// comprobamos que los tipos e identificadores de los datos miembro han sido reflexionados
// correctamente mediante las siguientes funciones consteval:
// · consteval auto type_of(std::meta::info r) -> std::meta::info
// · consteval auto identifier_of(std::meta::info r) -> std::string_view
static_assert(std::same_as<typename[: std::meta::type_of(nsdms[0]) :], int>); // ok
static_assert(std::same_as<typename[: std::meta::type_of(nsdms[1]) :], double>); // ok
static_assert(std::meta::identifier_of(nsdms[0]) == "x"); // ok
static_assert(std::meta::identifier_of(nsdms[1]) == "y"); // ok
auto s = S{}; // inicializamos un objeto de tipo S
s.[:nsdms[0]:] = 1; // el compilador lo traduce a: s.x = 1;
s.[:nsdms[1]:] = 2.5; // el compilador lo traduce a: s.y = 2.5;
auto p = &[:nsdms[0]:]; // puntero a dato miembro
static_assert(std::same_as<decltype(p), int S::*>); // ok
s.*p = 2; // ahora x == 2
Emplearemos habitualmente las siglas nsdms para referirnos al conjunto de datos miembro no-estáticos (non-static data members) de una clase.
Nota 1: semántica de valor en reflexión
La reflexión en C++26 adopta una estrategia de semántica de valor, permitiendo que las funciones del espacio de nombres std::meta sean resueltas mediante Argument-Dependent Lookup (ADL). Esto hace posible omitir la cualificación explícita del espacio de nombres cuando se considere adecuado, mejorando la ergonomía sintáctica.
Sin embargo, y con el fin de evitar cualquier posible ambigüedad, a lo largo del artículo seguiremos empleando la forma totalmente cualificada (std::meta:: o alias equivalente) en todas las llamadas a función.
Nota 2: Cadenas con almacenamiento estático
En la API de reflexión, las funciones que devuelven un string_view proporcionan vistas a datos almacenados de forma estática, por lo que no existe el riesgo de tratar con punteros colgantes (dangling pointers).
Las funciones relevantes en este sentido, definidas en el espacio de nombres std::meta, son symbol_of, identifier_of y display_string_of, además de sus equivalentes con prefijo u8 (que retornan u8string_view).
En el código anterior, la función del espacio de nombres std::meta
consteval auto nonstatic_data_members_of(
std::meta::info type,
std::meta::access_context ctx
) -> std::vector<std::meta::info>;
devuelve un secuencia std::vector de meta-valores std::meta::info, uno por cada dato miembro no-estático declarado directamente (no heredado) en el tipo representado por type, preservando el orden original. Esta función resultará de gran utilidad, tanto en este artículo como en futuras entregas de la serie. Véase la "Nota 4" para más detalles acerca de la necesidad de emplear define_static_array().
nota 3: Contextos de acceso
C++26 contempla varios posibles contextos de acceso en los que se realizan consultas reflexivas [8]. La clase std::meta::access_context representa un espacio de nombres, clase o función desde donde se pueden realizar consultas relativas a reglas de acceso, así como la clase designante, si existiera. Posee un ámbito (función miembro scope()) y una clase designante (designating_class()) asociadas.
En C++, una expresión E que designa a un miembro m posee una clase designante que condiciona el acceso a m. Dicha clase designante se determina según dos reglas:
- Si E es un splice-expression, es la clase más interna de la cual m es miembro directo.
- En cualquier otro caso, es la clase en cuyo ámbito la búsqueda de nombres (name lookup) halló a m.
Distinguimos:
- access_context::current(): la clase designante es la reflexión nula (es decir, el valor std::meta::info{} que no representa a ninguna entidad del programa) y el ámbito es la función, clase o espacio de nombres desde donde se evalúa la llamada. Se emplea para comprobar qué miembros pueden inspeccionarse según las reglas habituales de acceso y amistad (friendship).
- access_context::unprivileged(): la clase designante es la reflexión nula y el ámbito es el espacio de nombres global. Se carece, así, de cualquier privilegio de clase o amistad, limitándose el acceso a miembros públicos exclusivamente.
- access_context::unchecked(): tanto la clase designante como el ámbito son la reflexión nula. No se aplican las comprobaciones normales de acceso y la reflexión es capaz de inspeccionar todos los miembros, incluidos aquéllos con modificadores private y protected.
- access_context::via(cls): mantiene el ámbito actual, pero establece cls como clase designante. Es la herramienta fundamental para evaluar si un miembro es accesible a través de una clase específica en una jerarquía de herencia (particularmente, miembros protected).
En el siguiente ejemplo, imprimimos el número de datos miembros no-estáticos de una clase accesibles según el contexto current/unprivileged/unchecked:
class S {
int a;
public:
double b;
// current() en una función de S accede a todos los miembros:
consteval auto num_nsdm_0() -> std::size_t {
return std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::current()).size();
}
// unprivileged() ignora los privilegios de S y accede sólo a los miembros públicos:
consteval auto num_nsdm_1() -> std::size_t {
return std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::unprivileged()).size();
}
friend consteval auto num_nsdm_3() -> std::size_t;
};
consteval auto num_nsdm_2() -> std::size_t {
// current() en función libre no friend; acceso a miembros públicos de S:
return std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::current()).size();
}
consteval auto num_nsdm_3() -> std::size_t {
// current() en función libre friend; acceso a todos los miembros de S:
return std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::current()).size();
}
consteval auto num_nsdm_4() -> std::size_t {
// unchecked() ignora todos los controles de acceso y accede a todos los miembros:
return std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::unchecked()).size();
}
static_assert(S{}.num_nsdm_0() == 2);
static_assert(S{}.num_nsdm_1() == 1);
static_assert(num_nsdm_2() == 1);
static_assert(num_nsdm_3() == 2);
static_assert(num_nsdm_4() == 2);
NOTA 4: SOBRE std::define_static_array
El lenguaje carece actualmente de un mecanismo general para asignar memoria en tiempo de compilación y que ésta persista en tiempo de ejecución (capacidad conocida como non‑transient constexpr allocation). A modo de ejemplo, el siguiente código no compila:
// error: 'std::meta::nonstatic_data_members_of(...)' is not a constant expression
// because it refers to a result of 'operator new'
constexpr auto nsdms = std::meta::nonstatic_data_members_of(^^S,
std::meta::access_context::current());
Como comprobaremos en esta serie, son numerosas las situaciones en las que resulta útil promover almacenamiento en tiempo de compilación a almacenamiento estático. Para ello, C++26 introduce las funciones consteval de alto nivel define_static_string, define_static_object, y define_static_array en el espacio de nombres std.
En particular, la función (el espacio de nombres std se asume implícito):
template<ranges::input_range R>
consteval auto define_static_array(R&& r) -> span<ranges::range_value_t<R> const>;
toma como argumento un rango de tipo ranges::input_range R, crea un array de almacenamiento estático con los contenidos del rango y retorna un span<T const> para el array, siendo T un alias para ranges::range_value_t<R> [4]. El tipo subyacente T debe ser estructural [9] (lo que excluye, en particular, a std::span y std::string_view), construible y copiable.
Esta es la clave para que, en el ejemplo señalado, el vector de valores std::meta::info devuelto por nonstatic_data_members_of pueda persistir fuera de la expresión constante.
Conviene resaltar que la reflexión en C++26 no deja expuesto el AST del compilador, sino que ofrece una proyección estandarizada de parte de él mediante meta‑valores
std::meta::info.
A lo largo de esta serie, iremos explorando los aspectos de la reflexión en C++ a través de varios ejemplos de nivel relativamente avanzado.
2. Parseador de opciones de línea de comandos
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) mediante reflexión, capaz de mapear automáticamente valores a un agregado de datos —sin macros y con total seguridad de tipos. Puede consultarse una codificación simplificada en el ejemplo 3.7 de la referencia [1].
El código puede compilarse con GCC (rama
trunk), utilizando
-std=c++26 y habilitando el soporte experimental de reflexión mediante
-freflection. El lector puede consultar la implementación en el siguiente enlace de
Compiler Explorer:
https://godbolt.org/z/4vTsv8K7b.
Por sencillez, nuestro analizador de línea de comandos admitirá únicamente opciones largas (long options), según el formato:
--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). Pueden consultarse varios ejemplo de uso en la sección final de este artículo.
Como primer paso, desglosaremos las cabeceras estándar necesarias para nuestro parseador:
#include <algorithm>
#include <array>
#include <charconv>
#include <concepts>
#include <cstdlib>
#include <expected>
#include <format>
#include <meta> // novedad en C++26
#include <ranges>
#include <span>
#include <sstream>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <vector>
así como una serie de alias para los espacios de nombres estándar que emplearemos en nuestro código:
namespace stdm = std::meta;
namespace stdr = std::ranges;
namespace stdv = std::views;
Observemos, en particular, la inclusión de la nueva cabecera <meta>, puerta de entrada al sistema de reflexión estática estándar en C++26.
El almacenamiento de las opciones se realizará en un agregado cuyos datos miembro no-estáticos deberán satisfacer el siguiente concepto parsable_option. Éste delimita el dominio semántico de los valores de opción (option_value) permitidos en nuestra interfaz de línea de comandos:
template<typename T>
concept istream_extractable = std::default_initializable<T> and requires (std::istream& is, T& t) {
{ is >> t } -> std::same_as<std::istream&>;
};
template<typename T>
concept parsable_option = std::same_as<T, std::remove_cvref_t<T>>
and (std::same_as<T, bool>
or std::same_as<T, std::string>
or std::is_arithmetic_v<T>
or istream_extractable<T>);
Admitimos, pues, tipos sin cualificadores const o volatile ni referencias, que sean booleanos, cadenas de caracteres, tipos aritméticos o, más en general, de cualquier otro tipo construible por defecto y extraíble desde un flujo de entrada istream. Las enumeraciones y los tipos opcionales std::optional, entre otros, podrían admitirse con relativa facilidad, pero se omitirán por simplicidad.
Definamos un parseador genérico de valores de opción, parse_option_value<T>(), que reciba una representación textual (string_view como "true", "5.7" o "[2,7]") y seleccione, en tiempo de compilación, la estrategia de parseo adecuada según el tipo T, produciendo un resultado std::expected [10]. Aquí, T representa uno cualquiera de los tipos de dato miembro no-estático de nuestro agregado. Si la representación textual es válida para T, devolveremos la conversión con éxito; en caso contrario, retornaremos un std::unexpected<Parse_value_error> que describa el motivo del fallo, distinguiendo si el valor es inválido (invalid) o está ausente (missing):
[[nodiscard]] constexpr auto to_lower_ascii(unsigned char c) noexcept -> unsigned char
{
return (c >= 'A' and c <= 'Z')? static_cast<unsigned char>(c - 'A' + 'a') : c;
}
// comparación ASCII case-insensitive, sin coste de asignaciones ni locale:
[[nodiscard]] constexpr auto equals_ascii(std::string_view sv1, std::string_view sv2) noexcept -> bool
{
auto tlwr = [](char c){ return to_lower_ascii(static_cast<unsigned char>(c)); };
return sv1.size() == sv2.size()
and stdr::equal(sv1, sv2, [&](char a, char b){ return tlwr(a) == tlwr(b); });
}
enum class Parse_value_error { invalid, missing };
template<parsable_option T>
[[nodiscard]] auto parse_option_value(std::string_view value) -> std::expected<T, Parse_value_error>
{
using enum Parse_value_error;
if constexpr (std::same_as<T, bool>) {
// interpretación flexible de valores booleanos (true en caso de omisión):
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 std::unexpected(invalid);
}
else if constexpr (std::same_as<T, std::string>) {
// se exigen valores explícitos no-vacíos para strings:
if (value.empty()) { return std::unexpected(missing); }
return std::string{value};
}
else if constexpr (std::is_arithmetic_v<T>) {
// parseo numérico eficiente mediante std::from_chars (restos no consumidos producen error):
if (value.empty()) { return std::unexpected(missing); }
auto res = T{};
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 std::unexpected(invalid); }
return res;
}
else if constexpr (istream_extractable<T>) {
// extracción desde un flujo con locale clásico (restos no consumidos producen error):
if (value.empty()) { return std::unexpected(missing); }
auto res = T{};
auto iss = std::istringstream{value};
iss.imbue(std::locale::classic());
if (not (iss >> res)) { return std::unexpected(invalid); }
iss >> std::ws;
if (not iss.eof()) { return std::unexpected(invalid); }
return res;
}
else {
std::unreachable(); // T no puede escapar a las categorías anteriores
}
}
El orden de las comprobaciones es, aquí, importante: dado que std::is_arithmetic_v<bool> es true, la rama específica de bool debe ir antes que la aritmética para que nuestra lógica personalizada (aceptar "yes", "no", etc.) tome prioridad. Los tipos char son también aritméticos: con el código actual, el valor 65 se parsearía a un dato miembro char como 'A'. Finalmente, el parseo numérico no soporta formato hexadecimal.
Observemos que la función aplica verificaciones estrictas a nivel de detección de contenido sobrante tras el parseo (por ejemplo, valores enteros como "4a" conducirán a error), así como rechazo explícito de formatos no válidos.
Finalmente, dado un agregado Opts, implementaremos la función parse_options<Opts>() que creará una instancia de dicho tipo y mapeará mediante reflexión los argumentos recibidos a los datos miembro no-estáticos correspondientes. Como ya establecimos, cada opción válida deberá aparecer en la forma --option_name option_value. La función identificará el dato miembro de Opts cuyo identificador coincide con option_name y le asignará el valor option_value previamente parseado al tipo adecuado. La función rechazará cualquier opción desconocida o argumento sobrante, de modo que sólo producirá un objeto Opts válido cuando todas las opciones coincidan exactamente con los datos miembro del agregado y todos los tokens de entrada hayan sido correctamente consumidos.
Para ello, definamos en primer lugar el concepto options_aggregate. Éste validará que un tipo dado T sea un agregado sin clases base y que todos sus datos miembro no-estáticos satisfagan individualmente el concepto parsable_option:
[[nodiscard]] consteval auto nsdms(stdm::info type) -> std::vector<stdm::info>
{
return stdm::nonstatic_data_members_of(type, stdm::access_context::current());
}
template<typename T>
[[nodiscard]] consteval auto all_parsable_options() -> bool
{
constexpr auto [...dm] = [:stdm::reflect_constant_array(nsdms(^^T)):];
return (... and parsable_option<typename[:stdm::type_of(dm):]>);
}
template<typename T> // concepto para agregado de opciones (empleando reflexión)
concept options_aggregate = stdm::is_aggregate_type(^^T)
and stdm::bases_of(^^T, stdm::access_context::current()).empty()
and all_parsable_options<T>();
Como explicamos anteriormente, el operador ^^T genera un meta-valor std::meta::info que representa al tipo T. A partir de él, la función nsdms() (un mero wrapper de nonstatic_data_members_of()) obtiene una secuencia std::vector de meta-valores std::meta::info, uno por cada dato miembro no-estático declarado directamente en T (no heredado). La función all_parsable_options comprueba que todos los datos miembros no-estáticos de T cumplan el concepto parsable_option. La restricción de que el agregado no pueda heredar de clases base responde a una decisión de diseño destinada a simplificar la implementación El código podría extenderse para soportar herencia utilizando funciones como bases_of y subobjects_of contenidas en <meta> [8].
NOTA 5: SOBRE std::meta::reflect_constant_array
La función del espacio de nombres std::meta
template<std::ranges::input_range R>
consteval auto reflect_constant_array(R&& r) -> std::meta::info;
transforma en tiempo de compilación un rango de tipo ranges::input_range R en un valor de reflexión std::meta::info que representa un array de almacenamiento estático de tipo const T[N], siendo T un alias para ranges::range_value_t<R> y N el tamaño del rango. Este array estático contiene los mismos valores que el rango original. El tipo subyacente T debe ser estructural, construible y copiable.
A diferencia de define_static_array(), que retorna un span del array, reflect_constant_array() nos devuelve un reflector al array. Ello nos permite usar un structured binding con pack de expansión ([...dm]) sobre el reflector, facilitando trabajar con todos los datos miembro del agregado de una sola vez en tiempo de compilación (en este caso, para comprobar que todos cumplen el concepto parsable_option con un fold expression).
De hecho, el estándar define define_static_array() en términos de reflect_constant_array() como [8]:
using T = ranges::range_value_t<R>;
meta::info array = meta::reflect_constant_array(r);
if (meta::is_array_type(meta::type_of(array))) {
return span<const T>(meta::extract<const T*>(array),
meta::extent(meta::type_of(array)));
} else {
return span<const T>();
}
La función parse_options<Opts>() toma entonces la forma:
template<options_aggregate T>
[[nodiscard]] auto is_identifier_of(std::string_view name) -> bool // ¿es 'name' un identificador de T?
{
static constexpr auto [...dm] = [:stdm::reflect_constant_array(nsdms(^^T)):];
return (... or equals_ascii(stdm::identifier_of(dm), name));
}
struct Parse_option_error {
std::string message;
};
template<options_aggregate Opts>
[[nodiscard]] auto parse_options(std::span<std::string_view const> args)
-> std::expected<Opts, Parse_option_error>
{
using R = std::remove_cv_t<Opts>;
auto res = R{}; // instancia del agregado que almacenará los valores parseados
auto is_name = [&](std::string_view str) -> bool {
return str.starts_with("--");
};
auto is_followed_by_value = [&](std::contiguous_iterator auto it) -> bool {
auto const next = it + 1;
return next != args.end() and not is_name(*next);
};
// validación temprana: emitimos error si el usuario ha introducido algún
// nombre de opción no definido en el agregado:
for (std::string_view arg : args | stdv::filter([&](auto arg){ return is_name(arg); })) {
auto const name = arg.substr(2); // "--option_name" --> "option_name"
if (not is_identifier_of<R>(name)) {
return std::unexpected{Parse_option_error{
.message = std::format("unknown command line option --{}", name)}};
}
}
// rastreo de tokens procesados en el span de argumentos:
auto consumed = std::vector<bool>(args.size(), false);
// iteración expansiva sobre la secuencia de meta-valores para
// los datos miembro no-estáticos del agregado:
template for (constexpr stdm::info dm : std::define_static_array(nsdms(^^R))) {
// identificador del dato miembro (por ejemplo, "radius" para el campo double radius):
constexpr auto identifier = stdm::identifier_of(dm);
// búsqueda lineal de dicho identificador en el span de argumentos:
if (
auto const it = stdr::find_if(args, [&](std::string_view arg){
return is_name(arg) and equals_ascii(arg.substr(2), identifier);
});
it != args.end() // true si el usuario ha introducido la opción --identifier
){
auto const has_value = is_followed_by_value(it);
auto const value_text = has_value? *std::next(it) : std::string_view{};
// reificación del tipo de dato miembro para instanciar el parseador de valores correcto:
using M = typename[:stdm::type_of(dm):];
if (auto const parse_result = parse_option_value<M>(value_text)) {
// si el parseo del valor textual tiene éxito, procedemos a asignarlo:
res.[:dm:] = std::move(*parse_result);
}
else { // propagamos el error en caso de parseo fallido
return std::unexpected{Parse_option_error{
.message = std::format("{} value for --{} (expected {})",
(parse_result.error() == Parse_value_error::missing) ? "missing" : "invalid",
identifier,
stdm::display_string_of(stdm::type_of(dm))) // texto legible del tipo (evitando alias)
}};
}
// marcamos el nombre de la opción (y, de existir, su valor asociado) como tokens consumidos:
auto const idx = std::distance(args.begin(), it);
consumed[idx] = true;
if (has_value) {
consumed[idx + 1] = true;
}
}
}
// comprobamos si existen tokens sin procesar, emitiendo error en dicho caso:
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{Parse_option_error{
.message = std::format("unused tokens remained: [{}]", leftovers)
}};
}
return res;
}
Notemos, en particular, que template for es una nueva sentencia en C++26 (denominada expansion statement) que permite la repetición en tiempo de compilación de una instrucción por cada elemento de:
- Una lista de expresiones.
- Cualquier entidad desestructurable mediante structured bindings (es decir, tipos tuple-like).
- Un rango cuyo tamaño sea conocido en tiempo de compilación.
En concreto, la sentencia de expansión:
template for (
init-statementopt
for-range-declaration : expansion-initializer
)
compound-statement
determina un tamaño de expansión expansion-size basado en el inicializador expansion-initializer y se expande posteriormente como [5]:
{
init-statementopt
additional-expansion-declarationsopt; // depende del tipo de expansión
{
for-range-declaration = E(0);
compound-statement
}
{
for-range-declaration = E(1);
compound-statement
}
// ... repetido hasta ...
{
for-range-declaration = E(expansion-size - 1);
compound-statement
}
}Dedicaremos el segundo artículo de esta serie a analizar en mayor profundidad los tipos de expansion statements permitidos.
Como aplicación práctica, consideremos el siguiente parseo de argumentos de ejecución argv. Recordemos, por completitud, que argv es un array de punteros char* que contiene los argumentos pasados al programa como cadenas de estilo C. argv[0], en particular, corresponde al nombre del programa (o la ruta con la que se ejecutó). argc es el número de argumentos recibidos (incluyendo argv[0]), siendo siempre mayor o igual a la unidad. argv[argc] es un puntero nulo.
// insertar aquí todo el código precedente de implementación del parseador
#include <print>
struct Color {
int r{}, g{}, b{};
};
auto operator>>(std::istream& is, Color& c) -> std::istream& {
int r, g, b;
char comma_1, comma_2;
if ((is >> r >> comma_1 >> g >> comma_2 >> b) and comma_1 == ',' and comma_2 == ',') {
c = Color{r, g, b};
return is;
}
is.setstate(std::ios::failbit);
return is;
}
struct Circle {
std::string name;
double radius{};
double border_width{};
double opacity{};
Color color{};
bool filled = false;
};
auto main(int argc, char* argv[]) -> int
{
auto c = parse_options<Circle const>(std::vector<std::string_view>{argv + 1, argv + argc});
if (not c) {
std::println("Error: {}", c.error().message);
return EXIT_FAILURE;
}
auto&& [name, radius, borderw, opacity, color, filled] = *c;
auto&& [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;
}
Los argumentos de ejecución
--name "Circle #1" --radius 9.4 --border_width 2 --opacity 0.8 --color 44,55,125 --filled
producirán la salida esperada:
name = Circle #1
radius = 9.4
border width = 2
opacity = 0.8
color = 44,55,125
filled = true
mientras que valores erróneos como
--name "Circle #1" --radius ra --border_width 2 --opacity 0.8 --color 44,55,125 --filled
conducirán a mensajes de error como
Error: invalid value for --radius (expected double)
Por su parte, la ausencia de valores esperados
--name "Circle #1" --radius --border_width 2 --opacity 0.8 --color 44,55,125 --filled
generará errores como
Error: missing value for --radius (expected double)
Los nombres de opciones que no encuentren correspondencia con los miembros del agregado serán también rechazados:
Error: unknown command line option --r
Finalmente, de permanecer tokens sin consumir (leftovers), obtendremos un mensaje de error:
i --name "Circle #1" 0.5 --radius 9.4 j
Error: unused tokens remained: [i, 0.5, j]
REFERENCIAS BIBLIOGRÁFICAS:
- P2996R13 – Reflection for C++26 – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p2996r13.html
- P3394R4 – Annotations for Reflection – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3394r4.html
- P3293R3 – Splicing a base class subobject – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3293r3.html
- P3491R3 – define_static_{string,object,array} – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3491r3.html
- P1306R5 – Expansion Statements – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p1306r5.html
- P3096R12 – Function Parameter Reflection in Reflection for C++26 – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3096r12.pdf
- P3560R2 – Error Handling in Reflection – https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/p3560r2.html
- Working Draft Programming Languages - C++ – Metaprogramming library – https://eel.is/c++draft/meta.reflection
- cppreference - Template Parameters – https://en.cppreference.com/w/cpp/language/template_parameters.html
- cppreference - std::expected – https://en.cppreference.com/w/cpp/utility/expected.html