Named function arguments

Document number:
D3777R0
Date:
2026-04-06
Audience:
EWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Reply-to:
Jan Schultke <janschultke@gmail.com>
Murat Can Çağrı <cancagri.dev@gmail.com>
Matthias Wippich <mfwippich@gmail.com>
Lénárd Szolnoki <cpp@lenardszolnoki.com>
Ville Voutilainen <ville.voutilainen@gmail.com>
Bengt Gustafsson <bengt.gustafsson@beamways.com>
GitHub Issue:
wg21.link/P3777/github
Source:
github.com/eisenwave/cpp-proposals/blob/master/src/named-args.cow

We propose a syntax for calling C++ functions with named arguments.

Contents

1

Introduction

1.1

Why not just use a struct and designated initializers?

1.1.1

No templates

1.1.2

Worse overload resolution

1.1.3

Complexity and inconvenience

1.1.4

Dilemma of choice

1.1.5

ABI and performance considerations

1.1.6

Freezing API

1.2

Other workarounds

2

Prior Art

2.1

N4172 "Named arguments"

2.2

P0671R2 "Self-explanatory function arguments"

2.3

P1229R0 "Labelled Parameters"

3

Motivation

3.1

Simplifying overload resolution

3.2

Improving error messages

3.3

Preventing common bugs

3.4

Avoiding magic numbers

3.4.1

Note on encouraging large parameter lists

3.5

Arbitrary keyword arguments

4

Design

4.1

Why require an explicit opt-in?

4.2

Mixing named and positional arguments

4.3

Forwarding named arguments

4.4

Restrictions on labeled types

4.5

Combining references and labeled types

4.6

Calling labeled parameters with positional arguments

4.7

Macro-friendliness

4.8

Out-of-order named arguments

4.9

Disqualifying overloads early

4.10

C compatibility

4.11

Cross-version compatibility

5

Impact on the standard

5.1

Integration into the standard library

6

Implementation experience

7

Wording

8

References

1. Introduction

We propose a syntax for declaring C++ functions with named parameters and for calling C++ functions with named arguments:

// Declare a function with three "labeled parameters". void f(int .x, int .y, int .z); // Call that function with named arguments, // not in the same order as declared. f(.x = 0, .z = 2, .y = 1);

A call syntax like this has been suggested multiple times historically in [N4172], [P0671R2], and [P1229R0], but never in a way that found consensus in WG21. Nonetheless, this shows that there is interest in the feature, and perhaps, if approached the right way, the idea could succeed.

1.1. Why not just use a struct and designated initializers?

A common criticism of the idea is that rather than using named arguments, one could use a struct.

struct lerp_args { double a, b, t; }; double lerp(lerp_args args); lerp({ .a = 10., .b = 20., .t = 0.5 });

While this seemingly solves the issue, upon further thought, this is not a good solution for the following reasons:

1.1.1. No templates

The lerp approach cannot be used in a template like template<class T> T lerp(lerp_args<T>), or at least, a call like lerp(lerp_args{/* ... */}) would be necessary. To be fair, this problem may be solved by permitting CTAD for function parameters, for some cases. It does not work for forwarding references. However, even if that approach worked, it would require the user to create really complicated class templates for parameter sets like the ones we have in <algorithm>.

1.1.2. Worse overload resolution

With the proposed semantics, overloads are eliminated as non-viable candidates extremely early, on the basis of not having matching parameter names. For the struct approach, when an overload cannot be called due to a name mismatch, this happens at a later stage: when forming implicit conversion sequences.

See also §3.1. Simplifying overload resolution.

1.1.3. Complexity and inconvenience

Users don't want to maintain these additional parameter structs. They make a simple problem of passing named arguments more tedious than it needs to be. Realistically, proposing to add an overload to std::lerp that took std::lerp_args would likely not be fruitful. While the existing declaration is obviously problematic, it would be a huge burden on the committee to maintain these additional argument structs. Library authors in general face the same problem.

1.1.4. Dilemma of choice

Even when designing new functions from scratch, it feels unnecessary and much more complex to pass all arguments via struct rather than simply writing a function. In a code base where passing arguments via struct is common, one constantly has to make the choice whether to pass normally, via struct, or perhaps both. This adds up to wasted developer time. Named arguments let us have our cake and eat it too.

1.1.5. ABI and performance considerations

In most ABIs, when a struct is sufficiently large, it will not be passed by register. Inlining can sometimes solve this issue, but especially for larger mathematical functions, it should not be relied upon, and these ABI issues can result in degraded performance.

Consider the following code sample (with bad and naive implementation of linear interpolation):

struct lerp_args { double a, b, t; } double lerp(double a, double b, double t) { return (1 - t) * a + t * b; } double lerp(lerp_args args) { return (1 - args.t) * args.a + t * args.b; }

The second overload is not only clunkier to write, it also results in worse -O2 codegen with Clang 21 (targeting the x86-64 psABI):

.LCPI0_0: .quad 0x3ff0000000000000 lerp(double, double, double): movsd xmm3, qword ptr [rip + .LCPI0_0] ; load 1.0 rip-relative subsd xmm3, xmm2 mulsd xmm1, xmm2 mulsd xmm0, xmm3 addsd xmm0, xmm1 ret .LCPI1_0: .quad 0x3ff0000000000000 lerp(lerp_args): movsd xmm1, qword ptr [rsp + 24] ; load t from stack movsd xmm0, qword ptr [rip + .LCPI1_0] ; load 1.0 rip-relative subsd xmm0, xmm1 mulsd xmm1, qword ptr [rsp + 16] ; load b from stack mulsd xmm0, qword ptr [rsp + 8] ; load a from stack addsd xmm0, xmm1 ret

This problem is extremely common, not specific to the x86-64 ABI. To name one other example, in the Basic C ABI for WebAssembly, lerp(double, double, double) would compile to (f64, f64, f64) parameters whereas lerp_args would be passed indirectly, i.e. via (i32) memory address.

These ABI problems make the prospect of using a struct extremely unattractive for high-performance mathematical functions. This is especially unfortunate considering that functions such as lerp and clamp could significantly benefit from argument names at the call site.

See also §3.3. Preventing common bugs.

1.1.6. Freezing API

With lerp as specified as above, the user is able to store and pass around lerp_args, meaning that any change to it, such as turning it into a class template could break API and ABI. In general, creating a struct for a set of parameters freezes that parameter set. This would be unacceptable in e.g. the C++ standard library. The functions specified there are deliberately stated to be "non-addressable" so that changes to overload sets can be made. For example, a single function in <algorithm> stated to take an iterator could actually be implemented using two separate overloads: one for forward iterators and one for random access iterators.

1.2. Other workarounds

While creating a struct is most commonly cited as a workaround, other options to provide named arguments are sometimes mentioned:

// proposed, for reference lerp(.a = 50, .b = 100, .t = 0.5); // "Fluent Interface" lerp{} .a(50) .b(100) .t(0.5) (); // "parameter types" lerp(Range{50, 100}, 0.5);

In short, Fluent Interfaces use member function chaining so that each argument is provided individually to some kind of "builder class". The parameter types technique involves various parameter-specific types rather than working with scalar values, which often makes misuse of functions less likely thanks to the type system. These workarounds share most of the problems described in §1.1. Why not just use a struct and designated initializers?. Situationally, they can be quite reasonable. For example, bundling the a and b parameters of lerp in a Range class does make it unlikely that the function is misused, and it's relatively lightweight.

As good as they sometimes are, these workarounds only chip away at the massive problems we have. For example, to prevent misuses of the string constructor, are we seriously proposing to write code like this in the future?

cout << string(100, 'a'); cout << string(size_arg{100}, char_arg{'a'});

Should C++ developers generally be expected to design their APIs like this? In practice, neither LEWG nor library developers at large want this. It takes an absurd amount of API design effort, extra written code, and slower compilation. A side-by-side comparison should make it obvious why these techniques are undesirable:

Parameter types Named arguments
struct size_arg { size_t value; }; struct char_arg { char value; }; // ... string(size_arg size, char_arg c); // ... cout << string(size_arg{100}, char_arg{'a'}); string(size_t size, char c); // ... cout << string(.size = 100, .c = 'a');

For the sake of simplicity, this comparison ignores some nuances, like basic_string, CharT, size_type, etc. With those details involved, parameter types become even less attractive because the technique would require creating a large amount of class templates and relying on CTAD rather than working with simple structs.

2. Prior Art

2.1. N4172 "Named arguments"

[N4172] proposed the following syntax:

void draw_rect(int left, int top, int width, int height, bool fill_rect = false); int main() { // Both of the function calls below pass the same set of arguments to draw_rect. draw_rect(top: 10, left: 100, width: 640, height: 480); draw_rect(100, 10, height: 480, fill_rect: false, width: 640); }

Functionally, our proposal is almost identical to [N4172], with a few key differences:

The proposal received overwhelmingly negative feedback, with the following poll being taken:

Poll: Should we encourage Botond to continue work on this proposal? Yes=6, No=11.

The major criticism were:

We believe that our proposal meaningfully addresses all of these criticisms. The language has evolved significantly since 2014, and the perspective on many of these issues changed.

2.2. P0671R2 "Self-explanatory function arguments"

[P0671R2] proposes the following syntax:

double Gauss(double x, double mean, double width, double height); Gauss(0.1, mean: 0., width: 2., height: 1.);

While the R2 proposal is strikingly similar to what we have (with different argument syntax though), R2 was never seen by the committee, and to our knowledge, it "died" in SG17.

However, R0 was seen in EWG at Toronto 2017, with largely negative feedback; see [MinutesP0671R0]. R0 has a syntax such as double !mean which is incompatible with existing declarations, so it is unclear how relevant the negative feedback still is. Much of the feedback revolved around API design and possible library workarounds, something that we address in great detail in §1. Introduction.

2.3. P1229R0 "Labelled Parameters"

[P1229R0] takes a dramatically different approach to the previous parameters. In essence, it adds the syntax:

// declaration void memcpy(to: void*, from: void*, n: size_t); // call memcpy(to: buf, from: in, n: bytes);

This syntax is sugar, and expands to the following underlying code:

// declaration void memcpy(std::labelled<std::label<char, 't', 'o'>, void*>, std::labelled<std::label<char, 'f', 'r', 'o', 'm'>, const void*>, std::labelled<std::label<char, 'n'>, size_t>); // call memcpy(std::labelled<std::label<char, 't', 'o'>>(buf), std::labelled<std::label<char, 'f', 'r', 'o', 'm'>>(in), std::labelled<std::label<char, 'n'>>(bytes));

Unsurprisingly, this idea did not find consensus. The paper was seen in SG17 at San Diego 2018 with negative feedback; see [MinutesP1229R0] (though we don't have record of polls):

Anon: (voted against seeing the paper again) I want the feature, but I want to use it everywhere, so I'm uneasy with functions having to take std::label. I want a more intrusive language feature

Anon: (voted against seeing the paper again) I want to see a more full-fledged feature for this. This paper seems like a half-measure. I want to be able to reorder, have optional parameters, etc.

Anon: (voted against seeing the paper again) this has extensive impacts on the type system, ADL, overloading, etc. These are complicated parts of the language. It introduces identifiers in new places, and they leak out of the function scope. These things weren't addressed in the paper, and I would want to see extensive exploration of these things.

We see further problems with [P1229R0]:

3. Motivation

3.1. Simplifying overload resolution

Named arguments can drastically simplify overload resolution by disqualifying (almost) all candidates on the basis of name mismatch.

When calling std::ranges::sort with named arguments, we can disqualify all but one overload based on name mismatch:

std::vector<Employee> employees = /* ... */; std::ranges::sort(.r = employees, .comp = compare_case_insensitive, .proj = &Employee::get_name);

By comparison, if we make a positional call with the three arguments, we have many potential candidates:

ranges::sort(I first, S last, Comp comp = {}, Proj proj = {}); // #1 ranges::sort(R&& r, Comp comp = {}, Proj proj = {}); // #2 ranges::sort(Ep&& exec, I first, S last, Comp comp = {}, Proj proj = {}); // #3 ranges::sort(Ep&& exec, R&& r, Comp comp = {}, Proj proj = {}); // #4

#1, #3, and #4 are only disqualified once template argument deduction has taken place; they are not viable due to constraints such as random_access_iterator not being satisfied.

Named calls can completely bypass complex processes such as template argument deduction, constraint satisfaction checks, or comparison of implicit conversion sequences when names don't match. This is expected to significantly improve compilation speed, especially for large and complex overload sets like the ones in <ranges>.

Adding named argument support to <ranges> is not part of this proposal, but the design allows for such support to be added subsequently without breaking changes. The long-term plan for the standard library should be to add named argument support wherever useful.

3.2. Improving error messages

Named calls are also tremendously useful when the user makes a mistake in calling a function in an overload set. Rather than requiring lengthy explanations about unsatisfied constraints or implicit conversions sequences that cannot be formed, the error message can simply say that a call was not possible due to argument and parameter name mismatches.

Assume that the user intended to call the S(int, int) overload in the following sample:

struct S { S(void* p); S(int x, int y); }; S s(100);

Clang 21 (similar to EDG 6.7) outputs the following error:

<source>:6:3: error: no matching constructor for initialization of 'S' 6 | S s(100); | ^ ~~~ <source>:1:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'int' to 'const S' for 1st argument 1 | struct S { | ^ <source>:1:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'int' to 'S' for 1st argument 1 | struct S { | ^ <source>:2:5: note: candidate constructor not viable: no known conversion from 'int' to 'void *' for 1st argument 2 | S(void* p); | ^ ~~~~~~~ <source>:3:5: note: candidate constructor not viable: requires 2 arguments, but 1 was provided 3 | S(int x, int y); | ^ ~~~~~~~~~~~~

The constructor the user actually wanted to call is found at the very end of the error message, buried under irrelevant explanations relating to why there is no viable conversion from int to const S in order to call the copy constructor, etc.

GCC 15 is even worse because it does not even mention the constructor the user intended to call; it speculates that we tried to call S(void*), which is wrong:

<source>:6:5: error: invalid conversion from 'int' to 'void*' [-fpermissive] 6 | S s(100); | ^~~ | | | int <source>:2:13: note: initializing argument 1 of 'S::S(void*)' 2 | S(void* p); | ~~~~~~^

MSVC 19.43 combines the worst of Clang and GCC; it lists every constructor except the relevant one:

<source>(6): error C2665: 'S::S': no overloaded function could convert all the argument types <source>(4): note: could be 'S::S(S &&)' <source>(6): note: 'S::S(S &&)': cannot convert argument 1 from 'int' to 'S &&' <source>(6): note: Reason: cannot convert from 'int' to 'S' <source>(4): note: or 'S::S(const S &)' <source>(6): note: 'S::S(const S &)': cannot convert argument 1 from 'int' to 'const S &' <source>(6): note: Reason: cannot convert from 'int' to 'const S' <source>(2): note: or 'S::S(void *)' <source>(6): note: 'S::S(void *)': cannot convert argument 1 from 'int' to 'void *' <source>(6): note: Conversion from integral type to pointer type requires reinterpret_cast, C-style cast or parenthesized function-style cast <source>(6): note: while trying to match the argument list '(int)'

The diagnostic quality for named calls is expected to be better:

S s(.x = 100);

This could hypothetically give us the following output:

<source>:6:3: error: no matching constructor for initialization of 'S' 6 | S s(.x = 100); | ^ ~~~ <source>:3:5: note: candidate constructor not viable: no argument for parameter 'y' provided 3 | S(int x, int y); | ^ ~~~ <source>:1:8: note: 3 other constructors are not viable because they have no 'x' parameter 1 | struct S { | ^

The problem in the example is not simply a quality-of-implementation issue; the compiler cannot magically guess which constructor we intended to call. Attempting initialization with the argument 100 could be an attempt at calling pretty much any constructor, and every compiler handles this situation in a uniquely broken way.

When we make calls with named arguments, the compiler can take a much better guess at which constructor we meant to call (presumably, the one with matching parameter names). This meaningfully addresses one of the greatest and longest-standing complaints that C++ users have: overly long and complicated errors.

3.3. Preventing common bugs

Functions such as std::lerp or std::clamp are extremely easy to misuse with positional arguments:

std::clamp(x, 0.f, 1.f); // OK, clamp x in [0, 1] std::clamp(0.f, x, 1.f); // compiles, but does the wrong thing std::clamp(0.f, 1.f, x); // compiles, but does the wrong thing

While the v, lo, hi order of parameters used in the C++ standard library is the most common convention, there is no universal rule for the signature of a clamp function, leading to this potential mistake. Confusingly, the "range arguments" are provided last for std::clamp but first for std::lerp, so the standard library is not even internally consistent about these things.

The same mistake is impossible with a named call:

std::clamp(.v = x, .lo = 0.f, .hi = 1.f); // OK std::clamp(.lo = 0.f, .v = x, .hi = 1.f); // OK std::clamp(.lo = 0.f, .hi = 1.f, .v = x); // OK

std::lerp and std::lerp are by no means the only examples of standard library functions where such mistakes can be made.

Despite the following line being an obvious mistake, no compiler issues a warning (at the time of writing) due to the fact that all conversions are value-preserving:

std::cout << std::string('a', 10);

This calls the string(size_type, char) constructor, resulting in 97 U+0010 END OF LINE characters being printed. Such a mistake is totally preventable:

std::cout << std::string(.ch = 'a', .count = 10);

Another possible source of mistakes lies in the fact that there are two competing conventions for how to order inputs and outputs in function signatures:

This inconsistency can easily result in mistakes, such as mixing up the source and destination buffer of memcpy because the user expected it to work like copy_n. These mistakes are generally impossible with named arguments because the position of the source and destination parameters are irrelevant.

3.4. Avoiding magic numbers

Named arguments can significantly improve the readability of a function call.

The following function call would raise the readability-magic-numbers ClangTidy diagnostic:

draw_text("awoo", 18, true);

The meaning of the arguments is clearer with names provided:

draw_text(.text = "awoo", .size = 18, .kerning = true);

Users have come up with variety of workarounds to avoid magic numbers/flags:

While all of these workarounds technically solve the problem, there is no clear answer to which of these is best, resulting in competing styles and dilemmas of choice. Named arguments would largely obsolete these workarounds.

At some large amount of parameters, users typically create structs to bundle up options, but even then, it's not obvious whether every parameter should be in that struct or if some could remain outside (e.g. a std::string_view text parameter). It is also not obvious how to handle smaller parameter lists of ≥ 3 parameters. Just three parameters can be mentally demanding, and are not usually enough to warrant a separate struct.

3.4.1. Note on encouraging large parameter lists

Some people may argue that named arguments would encourage users to write functions with overly many parameters. We argue that the amount of parameters to a program's subroutine is innate. Whether parameters are provided as function parameters, a struct, options for a builder class, or something else, the amount of parameters remains the same; text rendering doesn't become any simpler just because the font size is in a struct. Parameters can only be shuffled into different places, and there is no inherent reason why a function parameter list would be a bad place compared to all these other alternatives.

3.5. Arbitrary keyword arguments

There are certain use cases where we would want to support a large set of "flags", "attributes", or "keyword arguments" in our functions. This is clearly illustrated by proposals such as [P2019R8], which proposes the following syntax:

std::jthread thr(std::thread::name_hint("worker"), std::thread::stack_size_hint(16384), [] { std::puts("standard"); });

Since the set of supported "thread attributes" may change in the future and may include implementation-defined attributes, we cannot simply use a struct aggregate that bundles up the attributes. Otherwise, future changes would be an ABI break.

With named arguments, and with the hypothetical syntax in §4.3. Forwarding named arguments (not yet proposed), we could solve this issue as follows:

namespace std{ template<class... Attr, meta::info... AttrNames> std::tuple<unspecified> kwargs(Attr&&... attr .AttrNames); class jthread { public: template<class... Attr, class F, class... Args> jthread(std::tuple<Attr...>, F&&, Args&&...); // ... }; } // Attr = [std::string_view, int] // AttrNames = ["name_hint", "stack_size_hint"], // i.e. we deduce a pack of argument name reflections std::tuple attr = std::kwargs(.name_hint = "worker"sv, .stack_size_hint = 16384); std::jthread thr(std::move(attr), [] { std::puts("standard"); });

This exact syntax may not actually be feasible or desirable, but the principle should be clear: arbitrary keyword arguments are much more concise and natural than creating a distinct type for each "attribute".

4. Design

To cornerstone of the design is the introduction of labeled parameters, which are parameters of labeled type. This makes parameter types with a label a distinct fundamental type that is tagged or labelled with the desired argument name.

In this code below, the function f has a single parameter of type int with label x.

int f(int .x) { // The label of the parameter is simultaneously the name // found by lookup within the function. // // "x" is an lvalue of type int inside here. return x; } using P = int .x; // Same syntax is used for the parameter type. int f(P); // OK, redeclares f

Similar to reference parameters, there is a mismatch between the type of the parameter and the type of the expression in the function:

void f(int &x, int .y) { x; // lvalue of type int, // but the parameter has declared type "reference to int" y; // lvalue of type int, // but the parameter has declared type "int with label y" }

This kind of type adjustment is crucial because otherwise access to y would have type int.y as if it was a named argument rather than just an access of the parameter's value.

4.1. Why require an explicit opt-in?

An obvious question is why an explicit opt-in should be necessary at all. After all, many other languages such as Kotlin allow any function parameter to be called with both positional and named syntax. However, we chose not to pursue this for a number of reasons:

  1. The massive amount of existing C++ code would all inadvertently opt into being called with named arguments. Parameter names in libraries were historically inconsequential and something that the library author can change arbitrarily. Even in C++26, the only way to observe parameter names is using reflection. It would be problematic to make the entire ecosystem shift towards parameter names becoming part of the API.
  2. After long discussion, we were unable to come up with an elegant solution to forwarding for parameter names. By comparison, our current design allows any existing emplace function to work with named parameters.
  3. If parameter names are not part of function type, detecting problems such as inconsistent parameters across TUs becomes difficult. If they are part of the type, that type is mangled.
  4. Function pointers or features such as std::function_ref could not interoperate without an explicit opt in. Our approach allows for std::function_ref<int(int .x)>.

4.2. Mixing named and positional arguments

We propose a heavily restricted form of mixing name and positional arguments. Namely, all positional arguments have to come first:

draw_text(.text = "awoo", .font_size = 18); // OK, named only draw_text("awoo", .font_size = 18); // OK, positional then named draw_text(.font_size = 18, "awoo"); // error: positional argument following named argument

This is useful because there are often tag parameters at the start, or parameters whose names are either obvious or irrelevant.

The following calls could be valid:

format("{x} {y}", .x = 10, .y = 20); emit_html_element("div", .id = "container");

4.3. Forwarding named arguments

Past proposals have faced concerns over the ability to forward argument names. This is something that works out of the box with our design because parameter names simply become part of the type.

template<class... Args> T& emplace(Args&&... args) { T* p = new (storage) T(std::forward<Args>(args)...); // ... return *p; } // OK, Args deduces to int.x, // i.e. "int with label x". emplace(.x = 100); // OK, Args deduces to int.&x, // i.e. "lvalue reference to int with label x". int value = 42; emplace(.x = value);

Note that the type adjustment which would discard the label from the type only takes place parameters where the label is not dependent. Perfect forwarding works because it is possible to form a reference to an object of labeled type such as int.x, and object of labeled type can be copied and passed around following all the usual language rules.

The difference can be observed as follows:

void f(int .x) { x; // lvalue of type int, not of type int.x } void h(auto y) { y; // lvalue of type int.y } g(.y = 0); // OK, deduces auto to int.y using P = int.z; void h(P p) { p; // lvalue of type int, not of type int.z } h(.z = 0); // OK void i(auto .w) { w; // lvalue of type int, not of type int.z } i(.w = 0); // OK

Here, the type adjustment does not take place for y because the label depends on a template parameter. However, type adjustment takes place for p.

Type adjustment also takes place for w because while the type is dependent, the label of the type is not (it is always w).

4.4. Restrictions on labeled types

Labeled types such as int.x are copyable, but not assignable. There is also no implicit conversion to their underlying type because this would drop the argument name, which violates user intent.

4.5. Combining references and labeled types

It is both possible to have a labeled reference and a reference to a labeled type:

Being able to form references to labeled types is crucial to enable perfect forwarding and to make labeled types not behave specially in generic code. Being able to form labeled references is necessary to have labeled parameters of reference type.

While the two are functionally very similar, the type adjustment rules are different.

void f(int .&x) { x; // lvalue of type int.x because we simply access what the reference refers to f(x); // OK, binds x to an lvalue of type int.x } void g(int &.x) { x; // lvalue of type int because the label is adjusted away, as is the reference g(x); // OK, implicit conversion of int to int&.x }

4.6. Calling labeled parameters with positional arguments

It is possible to call a function with a labeled parameter using a positional argument. This is a hugely important design aspect because

void print_number(int x, int .base = 10); print_number(100, 16); // Confusing and unclear. What does 16 mean here? print_number(100, .base = 16); // Better: we know what "16" means now. constexpr int base = 16; print_number(100, .base = base); // Redundant. print_number(100, base); // Better: no need to repeat ourselves.

The reason why print_number(100, 16) works is that an implicit conversion sequence from 16 (which is a prvalue of type int) to int .base is formed. This is treated like an identity conversion sequence for the purpose of overload resolution. Such a labeling conversion preserves the value of the expression and value category exactly, so it only exists on paper, much like a conversion from int to int.

Crucially, an argument of labeled type cannot be converted to another labeled type because this would alter the name against the user's intent. It also cannot be converted to its underlying type.

4.7. Macro-friendliness

A potential issue with the syntax int .x is that the identifier x can no longer be used as the name of a macro. When a large library opts into named parameters in large amounts of code, this could have some chance of conflicting with macros defined by the user. The standard library uses reserved names like __param or _Param to avoid this conflict.

To solve this problem, we propose an alias template that enables spelling a labeled parameter using a string literal:

// Similar trick to std::constant_wrapper to allow passing string literals: template<class T, cw-fixed-value Label> using labeled = __builtin_labeled_type(T, Label.data);

This alias template permits the user to write the following:

void f(int .x); // OK void f(std::labeled<int, "x">); // OK, redeclaration of f

While it would also be possible to have a builtin syntax like int ."x" rather than std::labeled<int, "x">, this whole problem is relatively minor anyway, and adding a second competing syntax appears excessive. Most C++ users happily use identifiers like x without needing to protect against macros.

4.8. Out-of-order named arguments

We also propose to allow for reordering of named arguments to fit the respective parameters.

void f(int .x, int .y); f(.y = 1, .x = 0); // OK, the first parameter is initialized to 0

This requires altering overload resolution so that named arguments can be mapped onto their respective parameters, prior to forming any implicit conversion sequence and even prior to template argument deduction.

void f(auto a, auto b); f(.y = 1, .x = 0); // OK, deduces first parameter to int.y, // but no attempt at reordering is made

4.9. Disqualifying overloads early

As explained in §3.1. Simplifying overload resolution, cutting down the number of candidate overloads via argument names is an important design goal. To do this, the disqualification would have to occur early in the process, even before template argument deduction.

Consider the following example with two (abbreviated) function templates:

void f(auto .x) { /* ... */ } void f(auto .y) { /* ... */ } f(.y = 0); // OK, calls second overload

A notably feature is that template argument deduction does not take place for f(auto .x); the entire candidate is disqualified because the argument is of a labeled type whose label does not match any parameter label in f.

This early disqualification only works if the label is not dependent; that is, it takes place in int .x and in auto .x but not in auto which deduced to int.x.

4.10. C compatibility

Surprisingly, even with the explicit opt-in syntax, it is possible to achieve C compatibility.

The following example shows a header that can be used in both C and C++:

#ifdef __cplusplus extern "C" void* memcpy(void* .dest, const void* .src, size_t .count); #else void* memcpy(void dest, const void* src, size_t count); #endif

C++ users can call memcpy using both named and positional arguments, whereas C users can only call memcpy using positional arguments. This approach relies on the fact that extern "C" disables mangling of the parameter names, so the underlying symbol is called memcpy. It also relies on the fact that a labeled type has the same value ABI properties as its underlying type, similar to how const int has the same ABI as int.

The design of the proposal can also be adopted by C in principle.

4.11. Cross-version compatibility

It may also be possible to extend this approach beyond extern "C" in order to reduce upgrade friction. For example, consider if we had a callable_nameless keyword to deactivate label mangling:

// OK, two declarations with distinct types: void f(int .x); void f(int); void f(int .x) callable_nameless; void f(int) callable_nameless; // error: cannot differ only in name

Crucially, this enables shipping a header with the signature void f(int .x) callable_nameless; in C++29 and void f(int); in any older version, down to C++98. Without any ODR violation, it would be possible to define the function in a source file later like:

void f(int .x) /* callable_nameless need not be repeated */ { // ... }

This means that conditional compilation is only ever needed in headers, not in source files.

5. Impact on the standard

In summary, a few parts of the core language are affected, such as function call syntax and overload resolution. The standard library impact is minimal because named argument support is not enabled by this paper. We merely add some policies in library wording.

5.1. Integration into the standard library

None of the proposals in §2. Prior Art discuss how named arguments could be integrated into the C++ standard library, despite this being an extremely important point of motivation and an important design aspect. There are a few key observations:

While allowing named arguments in the standard library is a long-term goal, we do not yet propose it here. Instead, we propose a general policy that the function parameter specified in the standard library are not stable and may be different in the implementation. This approach has precedent for SFINAE/expression-validity testing, for reflecting on standard library declarations, and other issues.

Subsequent proposals would gradually enable named arguments on a per-header basis. For headers that are "named-argument-enabled", [[positional]] would be specified on the few functions that require parameter name instability. While named arguments are not always useful (e.g. what's the point of sqrt(.x = 2)?), they are rarely harmful and rely on a parameter name that is likely to change, especially in a document as stable as the C++ standard.

Any function that has reserved identifiers as parameter names (e.g. f(int __x)) is recommended to behave as if [[positional]] was implicitly applied to it, meaning that to implement this proposal, no standard library code would need to be changed.

6. Implementation experience

See [ClangFork] for an experimental implementation.

7. Wording

None yet. Will be provided in a future revision once some design aspects have settled.

8. References

[N4172] Ehsan Akhgari, Botond Ballo. Named arguments 2014-10-07 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4172.htm
[N5014] Thomas Köppe. Working Draft, Programming Languages — C++ 2025-08-05 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2025/n5014.pdf
[P0671R2] Axel Naumann. Self-explanatory Function Arguments 2018-05-07 https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0671r2.html
[ClangFork] Murat Can Çağrı. named-args branch of term-est/llvm-project repository https://github.com/term-est/llvm-project/tree/named-args