Towards C++ kwargs

28 Dec 2024

(updated: 8 Jul 2025)


In the time since first writing this post, there have been a number of changes in the proposals and the committee has voted to accept a number of static reflection proposals into the draft C++26 standard.

Still, everything in this article is subject to change as ISO C++26 is being finalized.

Revisions as of 8 July 2025:

tl;dr A working example can be found on Godbolt.

§ Motivation

One of my favorite features from Python is the **kwargs syntax, for example, if we have a function defined as

def start_server(hostname: str, port: int = 8000, ssl: bool = False):
    # do things here

Then, we could turn on just the SSL feature while utilizing the default value for port by specifying a keyword argument.

start_server("localhost", ssl=True)

The upcoming C++26 standard has incorporated the "Reflection for C++" and function parameter reflection proposals enabling static reflection.

The aim of these proposals is to enable reflective metaprogramming within C++. With this we can not only observe structures but also generate code dependent on these observations. In the case of this post, we are able to observe a function parameters to generate structs with members based on the function's parameters.

§ Reflective Metaprogramming

The syntax for reflection is the unary ^^ operator [P3381R0].

Because reflection is such a fundamental (and primitive) operation for metaprogramming, it should have a simple spelling. The ^^ is intended to imply "lifting" or "raising" a term into the metaprogramming environment

[P2320R0]

The operator yields the meta::info scalar type, and we say that ^^thing is the reflection of thing.

We for now restrict the space of reflectable values to those of structural type in order to meet two requirements:

  1. The compiler must know how to mangle any reflectable value (i.e., when a reflection thereof is used as a template argument).

  2. The compiler must know how to compare any two reflectable values, ideally without interpreting user-defined comparison operators (i.e., to implement comparison between reflections).

[P2996R8]

Let us write a start_server in C++ for which we may wish to use kwargs with.

void start_server(std::string_view hostname, short port=8000, bool ssl=false) {
    std::print("Listening on http{}://{}:{}"sv,
                ssl ? "s" : "", hostname, port);

    // actually start the server
}

§ Function Parameters

With [P3096] we are able to obtain the reflection of a function and inspect its parameters with parameters_of.

constexpr auto parameters = std::meta::parameters_of(^^start_server);

This yields a std::vector<std::meta::info> of each of the parameters accepted by the function.

§ Default Parameters

On the topic of default parameters, the proposal states

"In our research, we have encountered the need to know whether a parameter has a default value, but not necessarily what the value is. [...] Therefore, we only propose to add the has_default_argument as it was already in [P1240R2]." [P3096R5].

For start_server we can observe that port and ssl have default values, but we are not able to retrieve what they are.

Until function reflection is amended to include this feature, we have to workaround it.

In this post, I opted to define a wrapper type default_argument<T, default_value> which has a template parameter for the default value. Unfortunately, this workaround means that we must rewrite our function to accomodate the kwargs metafunction.

template <typename T, T default_value>
struct default_argument {
    T value;
    constexpr default_argument(T t = default_value) : val(t) {};
    constexpr T operator*() { return value; }
};

void start_server(
    std::string_view hostname,
    default_argument<short, 8000> port={},
    default_argument<bool, false> ssl={}
) {
    std::print("Listening on http{}://{}:{}"sv,
                *ssl ? "s" : "", hostname, *port);
}

This creates a container where the constructor will take the default value from the template argument. However, this only works if T is a structural type; so we can't use things like std::string_view as in the hostname argument.

§ Parameter Vector

Now, we can iterate through each of the function parameters with template for

template for(constexpr auto e : meta::parameters_of(^^start_server)) { /* loop */}

Unfortunately, there are still a few defects in the experimental compiler (as of d77eff1) so we are unable to use template for in a consteval context even if the result parameters_of is known at compile-time.

Here, we turn to using the expand workaround mentioned in the "Implementation Status" section of [P2996R8].

[:expand(meta::parameters_of(^^start_server)):] >> [&]<auto e> { /* loop */ };

Now we can print things like the type and name of the parameters

[:expand(parameters):] >> [&]<auto e> {
    std::print("{} {}\n"sv,
                meta::identifier_of(meta::type_of(e)),
                meta::identifier_of(e));
};

As suggested by the names,

  • type_of gives us ^^type, i.e. the reflection of the type
  • identifier_of gives us the representation, i.e. variable name or type name

§ Parameter Structs

We can define a generic kwargs struct that accepts a template parameter of type meta::info.

template<meta::info>
struct kwargs;

This enables us to create a specialization for kwargs<^^function> whose members reflect those of the parameters of function.

Ideally, we would be able to use static reflection to create a specialization for our start_server function that is the same as the snippet below.

template<>
struct kwargs<^^start_server> {
    std::string_view hostname;
    default_argument<short, 8000> port;
    default_argument<bool, false> ssl;
}

After obtaining the parameters of a function, we can iterate through them using template for and create data member descriptions based on them. With these data member descriptions, we can use define_aggregate to apply the descriptions and complete the kwargs<^^function> specialization.

template<meta::info func_Ty>
consteval void make_function_struct() {
    std::vector<meta::info> nsdms;

    template for(constexpr auto e : parameters_of(func_Ty)) {
        nsdms.push_back({
            type_of(e),
            { .name = identifier_of(e) }
        });
    }

    return define_aggregate(^^kwargs<func_Ty>, nsdms);
}

// Example: Defines specialization kwargs<^^start_server>.
consteval {
    make_function_struct<^^start_server>();
}

Even if we are able to get the default argument value that [P3096R5] omits; the meta::data_member_spec [P2996R8] does not have the ability to set a default value for a member. However, we are able to both construct a kwargs<^^function> and initialize its members via meta::substitute; but this is outside the scope of this post.

The above code does not currently work [d77eff1]; simply replace template for with expand().

§ kwargs

With the parameter structs defined, we are now able to define an overload accepting a kwargs<^^function> as its only parameter, e.g.

void start_server(kwargs<^^start_server>&& params) {
    /* start_server(**params) */
}

Our goal is to create a parameter pack from the kwargs<^^start_server> struct we created using reflection and to pass it to the original function definition.

To accomplish this, I created a get<N>(kwargs<^^function>) function that enables us to access the data member of kwargs<^^function> corresponding to the Nth element.

// Example
auto data = kwargs<^^start_server> {
    .hostname = "10.0.0.1"sv,
    .port = 1234
};

assert(get<0>(data) == "10.0.0.1"sv);

The get function can be

consteval auto member_at(meta::info Ty, size_t i)
{
    meta::nonstatic_data_members_of(Ty)[i];
}

The get function utilizes the metafunction nonstatic_data_members_of to obtain the data members of kwargs<^^function> and a member slice on the Nth data member to obtain its value.

template<size_t N, typename T>
auto get(T const& kwargs)
{
    constexpr auto ctx = meta::access_context::current();
    constexpr auto idx = meta::nonstatic_data_members_of(^^T, ctx)[N];

    return kwargs.[:idx:];
}

Then, we can expand the kwargs<^^function> parameter struct into an argument list using a fold expression on get<N>.

void start_server(start_server_parameters&& params)
{
    constexpr auto ctx = meta::access_context::current();
    constexpr auto num_params =
                        meta::nonstatic_data_members_of(
                            ^^funcname##_parameters, ctx).size();

    [&]<size_t... I>(std::index_sequence<I...>) {
        start_server(get<I>(params)...);
    }(std::make_index_sequence<num_params> {});
}

§ Examples

Non-exhaustive list of examples

// Example 1: original
start_server("nguyen.vc"sv, 443, true);
// Output: Listening on https://nguyen.vc:443

// Example 2: original
start_server("localhost"sv);
// Output: Listening on http://localhost:8000

// Example 3: original, but only want to change ssl=true
start_server("localhost"sv, 8000, true);
// Output: Listening on https://localhost:8000

// Example 4: kwargs changing ssl only
start_server({ .hostname = "localhost"sv, .ssl = true });
// Output: Listening on https://localhost:8000

§ Positional Arguments

It is possible to extend the above to create an overload that accepts the non-default positional arguments with the kwargs object.

Currently, kwargs must preceed the positional arguments if we use a variadic template to define the overload which leads to an awkward syntax.

template<typename... Args>
start_server(kwargs<^^start_server>&&, Args... args)

Should non-terminal variadic template parameters [P0478] make it into the standard, we can then define an overload with func(Args... args, kwargs<^^func>&&), which brings us pretty close to Python's kwargs.

It is a rather straight-forward modification to accept a parameter pack in addition to positional arguments.

template <
    meta::info ctx = meta::access_context::current(),
    size_t paramc = meta::nonstatic_data_members_of(^^kwargs<^^start_server>, ctx).size(),
    typename... Args>
decltype(auto) start_server(kwargs<^^start_server>&& params, Args... args)
{
    constexpr auto argc = sizeof...(Args);

    return [&]<size_t... I>(std::index_sequence<I...>) {
        return start_server(std::forward<Args>(args)..., get<argc + I>(params)...);
    }(std::make_index_sequence<paramc - argc> {});
}

Here we use all of the positional arguments and obtain any missing arguments from the kwargs object.

As of the time of writing the original post, there was a defect in the P2996 implementation that forced me to define paramc as a template parameter rather than a constexpr variable within the function. As of 8 July 2025 [commit d77eff1], this seems to no longer be the case.

§ Duplicated Arguments

A feature that would be nice to have would to disallow multiple values for a parameter.

As it stands, providing a positional argument that is defined by the user in the kwargs object is not an error; the corresponding positional argument is taken over the value provided in the kwarg.

A potential way to detect this is to modify default_argument to have a user_defined flag.

template <typename T, T default_value>
struct default_argument {
    bool user_defined = false;
    /* ... rest of def'n ... */
};

Next, in the kwargs function overload we can check over each positional argument, ensuring that there is not a value in kwargs<^^function> that is flagged as user-defined.

[&]<size_t... I>(std::index_sequence<I...>) {
    (assert(!get<I>(params).user_defined), ...);
}(std::make_index_sequence<argc> {});

Again this only works if every argument is encapsulated with default_argument; as before, only arguments with structural types can be wrapped.

§ Examples

start_server({ .ssl = true }, "localhost"sv);
// Output: Listening on https://localhost:8000

start_server({ .port = 7777 }, "127.0.0.1"sv);
// Output: Listening on http://127.0.0.1:7777

start_server({ .hostname = "localhost"sv, .port = 7777, .ssl = true }, "127.0.0.1"sv);
// Output: Listening on https://127.0.0.1:7777

§ Code

Below is a definition for a make_function_kwargs macro that accepts a function name and generates a corresponding kwargs<^^function> specialization and an overload which accepts kwargs<^^function> and positional arguments.

Ideally, if we had code injection, we could create a metafunction that takes a reflection of a name space, and

  1. gets all functions within the namespace,
  2. injects kwargs<^^function> specializations for each function, and
  3. injects a kwargs overload into the same namespace.

However, the third point will remain elusive even with C++26 as the draft does not include proposals such as [P3294] which add the necessary mechanisms to inject new functions.

namespace meta = std::meta;
namespace views = std::views;

template <meta::info Ty>
consteval auto member_at(size_t i)
{
    return meta::nonstatic_data_members_of(
        Ty, meta::access_context::current()
    )[i];
}

template <size_t i, typename T>
auto get(T const& kwargs)
{
    return kwargs.[:member_at<^^T>(i):];
}

template <meta::info func_Ty>
struct kwargs;

template <meta::info func_Ty>
consteval auto __make_function_kwargs()
{
    static_assert(is_function(func_Ty));

    std::vector<meta::info> nsdms;

    [:expand(parameters_of(func_Ty)):] >> [&]<auto e> {
        nsdms.push_back(meta::data_member_spec(
            type_of(e), { .name = identifier_of(e) }));
    };

    return define_aggregate(^^kwargs<func_Ty>, nsdms);
}

#define make_function_kwargs(function)                                              \
    consteval { __make_function_kwargs<^^function>(); }                             \
    template <                                                                      \
        meta::access_context ctx = meta::access_context::current(),                 \
        size_t paramc = meta::nonstatic_data_members_of(                            \
            ^^kwargs<^^function>, ctx).size(),                                      \
        typename... Args>                                                           \
    decltype(auto) function(kwargs<^^function>&& params, Args... args)              \
    {                                                                               \
        constexpr auto argc = sizeof...(Args);                                      \
        return [&]<size_t... I>(std::index_sequence<I...>) {                        \
            return function(std::forward<Args>(args)..., get<argc + I>(params)...); \
        }(std::make_index_sequence<paramc - argc> {});                              \
    }

Defining an overload this way is a hack.

After we declare the start_server overload the token start_server now refers to an overload set, and the compiler is unable to resolve which overload to use for ^^start_server [P2996R12]. However, as we limit ourselves to only using ^^start_server in the declaration of the overload, i.e. before the completion of the overload declaration, the reflection ^^start_server will refer to the original declaration.

A possible resolution would to obtain all the members of the enclosing namespace and selecting the member in the overload set that does not accept a kwargs<^^function> object.

Another would to cause make_function_kwargs to fail if the provided reflection refers to an overload set. The check would only occur when we apply the metafunction, and there is no way to enforce the "uniqueness" of a function afterward potentially allowing a user to erroneously define an overload.

§ Conclusion

Static reflection and metaprogramming are powerful tools, and now that constexpr and consteval have matured this is a new and promising path for future C++.

This was an exercise for me to get familiar with reflection as they currently stand in draft C++26, and I do not expect myself to be updating this unless if I see a trick to obtain default arguments.

As for kwargs, despite how much I love this feature in Python, I do not see it to be nearly as practical in C++. This is mainly due to the fact that the language suports function overloading which will make it difficult to come up with a set of rules to resolve the proper overload for which to apply keyword arguments – as discussed above.

Some things I'd like to see in the future include the approval of [P0478], and an amendment to §10 of [P3096] (or a proposal) which includes some sort of get_default_argument metafunction to obtain the default value of an function argument.