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:
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, comprobamos 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 (públicos y privados):
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 e inmutable con los contenidos del rango y retorna un span<T const> para el array, siendo T un alias de ranges::range_value_t<R> [4]. Dicho tipo subyacente T debe ser estructural [9] —como es el caso de std::meta::info, pero no, en particular, de std::span o 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.
2. Sentencias de expansión (template for)
La nueva sentencia de expansión template for en C++26 (en inglés, expansion statement) permite la repetición en tiempo de compilación de un grupo de sentencias (compound-statement) por cada elemento de
- una lista de expresiones (expansión de enumeración),
- cualquier entidad desestructurable mediante structured bindings (es decir, tipos tuple-like; se habla entonces de una expansión de desestructuración), o
- un rango cuyo tamaño sea conocido en tiempo de compilación (expansión de iteración, el caso más habitual en reflexió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
}
}Las additional-expansion-declarations (de existir) son declaraciones internas generadas por el compilador para proceder con la iteración. Tanto dichas declaraciones como expansion-size y E dependen del expansion-initializer. En base a la categoría de expansión, distinguimos:
- Expansión de enumeración: No se requieren declaraciones adicionales additional-expansion-declarations, ya que los elementos se toman directamente de la lista de expresiones proporcionada. En este caso, expansion-size es el tamaño de la lista y E(i) es la expresión i-ésima de la lista.
- Expansión de desestructuración: se realiza un structured binding interno (similar a auto&& [u0, u1, ..., uexpansion_size-1] = expansion_initializer) para descomponer el objeto en sus elementos individuales. En este caso, expansion-size es el tamaño del structured binding y E(i) es ui.
- Expansión de iteración: se crean variables ocultas constexpr para el rango y los iteradores de inicio (begin) y fin (end). Éstos deben poder evaluarse en tiempo de compilación para determinar el número de veces que se repetirá el cuerpo del bucle. En este caso, expansion-size es la distancia de begin a end y E(i) es *(begin + i). Obsérvese que esta última operación de desreferencia exige que el rango sea de acceso aleatorio.
Puede incluirse el especificador
constexpr en el
for-range-declaration (como en
template for (constexpr auto&& elem : ...)) para forzar que cada elemento de la expansión sea tratado como una constante en tiempo de compilación. Esto es requisito indispensable si, por ejemplo, deseamos usar dicho elemento dentro de un
static_assert o como argumento de plantilla.
EJEMPLOS
Expansión de enumeración:
template<typename... Ts>
auto print_values(Ts const&... vs) -> void
{
template for (auto const& v : {vs...}) {
std::println("{}", v);
}
}
print_values(1, 7.8, "Paris"); // imprime: 1 7.8 Paris
Expansión de desestructuración:
auto tup = std::tuple{1, 7.8, "Paris"};
template for (auto const& elem : tup) {
std::println("{}", elem);
} // imprime: 1 7.8 Paris
Expansión de iteración:
template for (constexpr int I : std::array{1, 2, 3}) {
static_assert(I < 4);
} // OK
Las sentencias de control de salto continue y break no ejercen control sobre el proceso de expansión de código en tiempo de compilación, sino sobre la evaluación de la sentencia en tiempo de ejecución. continue salta al inicio de la siguiente iteración y break salta al final de la última iteración [5].
Aunque
template for resulta útil más allá de la meta-programación reflexiva, su inclusión en el estándar responde principalmente al esfuerzo por estandarizar la reflexión.
3. Caso de uso: parseador de opciones de línea de comandos
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. En este artículo, en particular, abordaremos un caso de uso canónico: el diseño mediante reflexión de un parseador de opciones de línea de comandos (Command‑Line Options), 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 16.1 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/a89zGfPs9.
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). Por diseño, nuestro parseador prohibirá la repetición de opciones: cualquier duplicado será tratado como un token no consumido. 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 <cstdio>
#include <cstdlib>
#include <expected>
#include <format>
#include <locale>
#include <meta> // novedad en C++26
#include <ranges>
#include <span>
#include <spanstream>
#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 desglose de cabeceras anterior puede sustituirse por la declaración de importación import std;
en los compiladores que así lo permitan:
#include <cstdio> // contiene la macro stderr
#include <cstdlib> // contiene las macros EXIT_SUCCESS y EXIT_FAILURE
import std;
Tal es el caso de GCC 16.1 mediante el comando de compilación g++ -std=c++26 -fmodules -freflection --compile-std-module main.cpp -o parser -lstdc++exp
.
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 (std::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 ascii_tolower(
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 ascii_iequals(
std::string_view sv1,
std::string_view sv2
) noexcept
-> bool
{
auto tlwr = [](char c){
return ascii_tolower(static_cast<unsigned char>(c));
};
return stdr::equal(sv1, sv2, {}, tlwr, tlwr);
}
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 ascii_iequals(value, "true") or
ascii_iequals(value, "1") or ascii_iequals(value, "yes") or
ascii_iequals(value, "y")) {
return true;
}
if (ascii_iequals(value, "false") or ascii_iequals(value, "0") or
ascii_iequals(value, "no") or ascii_iequals(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::ispanstream{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 un dato miembro char como 'A'. Finalmente, indicar que 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.
Comencemos definiendo el concepto options_aggregate. Éste validará que un tipo dado T sea un agregado sin clases base y que cada uno de sus datos miembro no-estáticos satisfaga, de manera individual, el concepto parsable_option. La reflexión nos permite articular dicha definición de forma directa:
[[nodiscard]] consteval auto nsdms_of(
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_of(^^T)) :];
return (... and
parsable_option<typename[: stdm::type_of(dm) :]>);
}
// concepto para agregado de opciones (empleando reflexión):
template<typename T>
concept options_aggregate =
stdm::is_aggregate_type(^^T)
and stdm::bases_of(
^^T, stdm::access_context::current()).empty()
and all_parsable_options<std::remove_cv_t<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_of() (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 miembro 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 la función bases_of (o bien, subobjects_of) contenida 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 de 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 std::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 reificado, 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 la función std::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>();
}
donde extract permite extraer un valor desde una reflexión cuando el tipo es conocido y extent retorna la longitud del array (volveremos a analizar estas funciones en próximos artículos de la serie).
La función parse_options<Opts>() toma entonces la forma:
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>;
// instancia del agregado que almacenará los valores parseados:
auto res = R{};
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);
};
// span de meta-valores representado los datos miembro no-estáticos
// del agregado:
static constexpr auto nsdms = std::define_static_array(nsdms_of(^^R));
// validación temprana: emitimos error si el usuario ha introducido
// algún nombre de opción no definido en el agregado:
// ¿es 'name' identificador del agregado?
auto is_identifier = [](std::string_view name) -> bool {
return [name]<std::size_t ...Is>(std::index_sequence<Is...>) {
return (... or ascii_iequals(stdm::identifier_of(nsdms[Is]),
name));
}(std::make_index_sequence<nsdms.size()>{});
};
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(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 los datos miembro no-estáticos
// del agregado:
template for (constexpr stdm::info dm : nsdms) {
// 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:
auto const it = stdr::find_if(args, [&](std::string_view arg){
return is_name(arg)
and ascii_iequals(arg.substr(2), identifier);
});
if (it == args.end()) {
continue; // el usuario no 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 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;
}
Mientras que la estructura del parseador se genera en tiempo de compilación mediante reflexión, el procesamiento de los tokens de opciones ocurre, lógicamente, en tiempo de ejecución.
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(stderr, "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]
4. Un apunte adicional [lectura opcional]
En ocasiones, puede resultar arquitectónicamente conveniente encapsular la lógica de reflexión en funciones auxiliares (típicamente consteval). Esta práctica permite desacoplar la meta-programación del flujo lógico tradicional, facilitando su mantenimiento y legibilidad. Sin embargo, este aislamiento puede imponer un coste en el rendimiento de ejecución, pues en ocasiones nos veremos obligados a recurrir a construcciones de borrado de tipos (type-erasure) o indirección —como std::variant, std::any o punteros a miembros. Se trata de un punto éste que el desarrollador debe evaluar cuidadosamente.
Así, en un caso como el considerado en este artículo, podríamos definir una plantilla de función consteval denominada nsdm_table<T>(), capaz de construir un std::array que asocie cada dato miembro no-estático de un tipo T con un objeto Nsdm_info, el cual contendrá:
- 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. Su empleo se justifica en el hecho siguiente: dado que distintos tipos de datos miembro conducen a tipos diferentes de punteros, es necesario un tipo suma que permita agrupar los objetos Nsdm_info en un único array homogéneo.
La estructura Nsdm_info queda definida como:
template<typename... Ts>
struct Nsdm_info {
std::string_view identifier;
std::variant<Ts...> member_ptr;
};
donde Ts... son las alternativas del variante. La función nsdm_table<T>() adopta entonces la forma siguiente:
template<typename T>
requires std::is_class_v<T>
[[nodiscard]] consteval auto nsdm_table()
{
constexpr auto nsdms = std::define_static_array(
stdm::nonstatic_data_members_of(
^^std::remove_cv_t<T>, stdm::access_context::current()));
static_assert(nsdms.size() > 0,
"nsdm_table<T>: T must have at least one non‑static data member");
return [&]<std::size_t... I>(std::index_sequence<I...>){ // (A)
static_assert((... and not stdm::is_reference_type(stdm::type_of(nsdms[I]))),
"nsdm_table<T>: reference members are not supported for member tables");
static_assert((... and not stdm::is_bit_field(nsdms[I])),
"nsdm_table<T>: bit-fields are not supported for member tables");
using Variant = std::variant<decltype(&[:nsdms[I]:])...>; // (B)
return std::array{ // (C)
Nsdm_info<decltype(&[:nsdms[I]:])...>{
.identifier = stdm::identifier_of(nsdms[I]),
.member_ptr = Variant{std::in_place_index<I>, &[:nsdms[I]:]} // (D)
}...
};
}(std::make_index_sequence<nsdms.size()>{});
}
- (A): Para generar la tabla de manera completamente estática y sin necesidad de bucles, definimos una lambda genérica sobre un pack de índices I.... make_index_sequence<N> produce la secuencia 0, 1, ..., N-1, donde N es el número de datos miembro no-estáticos accesibles de T. La lambda se invoca inmediatamente con esa secuencia (técnica IILE, véase este post para más detalles) y la expansión I... permite construir un objeto Nsdm_info por cada miembro. De forma alternativa, podría emplearse una expansión template for.
- (B): 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 (no se admiten bit-fields ni referencias). La declaración decltype(&[:nsdms[I]:]) nos permite determinar en tiempo de compilación el tipo del puntero al dato miembro I-ésimo.
- (C): Generamos un std::array de objetos Nsdm_info y longitud N=sizeof...(I), que será empleado posteriormente como tabla de despacho. Para el dato miembro I-ésimo:
- identifier almacena el nombre del miembro (identifier_of(nsdm[I])), y
- member_ptr almacena el puntero al miembro (&[:nsdms[I]:]), convenientemente empaquetado en el variante. A modo de ejemplo, dado el miembro double radius en un agregado de nombre Circle, identifier contendrá la cadena "radius" y member_ptr el puntero a miembro &Circle::radius, de tipo double Circle::*.
- (D): Es importante notar que varias alternativas del variante pueden tener el mismo tipo (por ejemplo, si T contiene varios int, las correspondientes alternativas serán todas de tipo int T::*). Por esta razón, el variante se inicializa mediante std::in_place_index<T> [11], asignando a cada dato miembro 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.
La función nsdm_table<T>() podría entonces emplearse para codificar parse_options<Opts>() abstrayendo por completo la lógica de reflexión de su definición. Esto evitaría, de considerarse conveniente, la necesidad de introducir construcciones de meta-programación explícitos en el cuerpo del parseador.
Ejemplo
Como ejemplo del modo de empleo de la función nsdm_table<T>(), se proporciona el código sencillo siguiente:
struct Velociraptor {
int health{};
double speed{}; // en km/h
};
constexpr auto table = nsdm_table<Velociraptor>();
auto raptor = Velociraptor{};
raptor.*std::get<int Velociraptor::*>(
table[0].member_ptr) = 100; // asignamos .health
raptor.*std::get<double Velociraptor::*>(
table[1].member_ptr) = 40; // asignamos .speed
std::println("{} = {} | {} = {} km/h",
table[0].identifier, raptor.health,
table[1].identifier, raptor.speed);
// imprime: health = 100 | speed = 40 km/h
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
- cppreference - std::in_place – https://en.cppreference.com/w/cpp/utility/in_place.html