· 6 min read ·

C++26 Settles the Comma Before the Ellipsis, and the Name Is Perfect

Source: lobsters

There is a change coming in C++26 that most working C++ programmers will never notice in their day-to-day code, yet it has one of the best names in recent standards history: the Oxford variadic comma. Sandor Dargo covers the mechanics, but the name itself deserves more unpacking than a single post can give it, because it encodes something true about how C++ evolves and where C-style variadics sit in the modern language.

The Syntax at Issue

C-style variadic functions have always been declared with ... at the tail of the parameter list:

void log(const char* fmt, ...);
int printf(const char* fmt, ...);

That comma before ... is what the debate is about. Historically, both of these were accepted by various compilers:

void f(int x, ...);   // with comma
void f(int x ...);    // without comma

The comma-free form is inherited from early C. In K&R C and through parts of the C89 era, compiler behavior around the ellipsis was inconsistent enough that some implementations treated the comma as optional. C++ eventually standardized on requiring the comma, but the comma-free form persisted in a kind of tolerated-but-not-quite-right limbo. C++26 ends that limbo by formally deprecating it.

The name is a play on the Oxford comma debate in English grammar: whether to write “eggs, toast, and juice” or “eggs, toast and juice”. The C++ committee has, in effect, ruled in favor of the Oxford comma for variadic syntax.

Why This Form Existed at All

To understand why the comma-free form was ever allowed, you have to go back to C’s original approach to variadic functions. In K&R C, before the ANSI standardization effort, the language had no formal mechanism for declaring that a function accepted a variable number of arguments. You just called functions with whatever arguments you wanted, and the calling convention did the rest. There was no ..., no va_list, no contract between caller and callee enforced at the language level.

C89/C90 introduced the formal variadic function syntax along with the <stdarg.h> machinery: va_list, va_start, va_arg, and va_end. The ... syntax became part of the standard, and the comma before it was formally required. But because C++ was being designed to absorb a large existing C codebase, and because compilers had varying interpretations of the older rules, some leniency crept in.

C23 made a similar tidying move on the C side of the fence, so C++26 is essentially keeping the two languages aligned in this corner of the grammar.

What Actually Changes

For most codebases, nothing changes at runtime. The deprecation affects only the syntactic form, not the behavior. Code written as:

void f(int x, ...) {
    va_list args;
    va_start(args, x);
    // ...
    va_end(args);
}

continues to work exactly as before. Code written in the comma-free form:

void f(int x ...) {  // now deprecated
    // ...
}

will generate a deprecation diagnostic. Nothing breaks immediately, but the intent is clear: this form is on the path to removal.

The practical effect is mostly on older codebases and generated code. If you have tooling that emits C++ function declarations, or if you maintain bindings to C libraries where the original headers used comma-free ellipsis syntax, those will eventually need updating.

The Broader Cleanup

This change is one of many small housekeeping items C++26 brings along. The committee has spent years deprecating and removing things inherited from C that never quite fit the C++ type system:

  • Trigraphs were removed in C++17, after decades of existing as a compatibility oddity almost nobody used intentionally.
  • The register keyword was deprecated in C++11 and removed in C++17.
  • C-style casts remain contentious but static_cast, reinterpret_cast, and const_cast give programmers explicit alternatives.
  • Variable-length arrays (VLAs) were never adopted into the C++ standard despite being in C99, an intentional divergence.

The Oxford variadic comma fits this pattern. It is not a safety issue; it is a grammar ambiguity that adds no expressive power and reflects a historical accident rather than a deliberate design choice.

C-Style Variadics in the Modern Language

Beyond the comma question, it is worth stepping back and considering where C-style variadics sit in C++ as a whole. They are, by any measure, one of the most dangerous features in the language. The va_arg macro requires you to pass the type of each argument explicitly, with no verification:

#include <cstdarg>

double sum(int count, ...) {
    va_list args;
    va_start(args, count);
    double total = 0.0;
    for (int i = 0; i < count; ++i) {
        total += va_arg(args, double);  // silent UB if wrong type passed
    }
    va_end(args);
    return total;
}

If a caller passes an int where you expect a double, you get undefined behavior with no diagnostic. The type information is simply not there at runtime.

C++11 gave programmers a way out with variadic templates:

template<typename... Args>
void log(Args&&... args) {
    (std::cout << ... << args);  // fold expression, C++17
}

This is fully type-safe. The compiler knows the type of every argument at every call site. C++17’s fold expressions made working with parameter packs substantially cleaner. C++23’s std::print and std::println address the most common reason C code reaches for printf, providing format-string type safety through the std::format machinery.

So the trajectory is clear: C-style variadics are a legacy mechanism kept for C compatibility and for extern "C" interfaces. The committee is not removing them, but it is tightening the grammar around them and signaling that new code should not depend on their specific quirks.

How Other Languages Handle This

It is instructive to look at how languages designed after C approached the variadic problem.

Go chose to make variadic functions type-safe and homogeneous: func f(args ...int) collects all arguments as a slice of the declared type. You can pass heterogeneous values only through an interface type, which preserves type information.

Rust essentially avoided the problem. Its macro system handles println! and friends at compile time, with full type checking. C-style variadics in Rust exist only for FFI, declared as extern "C" fn f(x: i32, ...) -> i32, and using them requires unsafe.

Java introduced varargs in 1.5 as syntactic sugar over arrays: void f(String... args) compiles to void f(String[] args). Type-safe, but only for homogeneous types.

Python’s *args and **kwargs carry full runtime type information, which sidesteps the problem at the cost of dynamic typing.

None of these languages have a comma debate because they designed their variadic syntax from scratch without inheriting decades of compiler variation. C++ is doing the slower, harder work of cleaning up a syntax that predates its own standardization.

Why the Name Gets It Right

The Oxford comma is genuinely contested in a way that most punctuation rules are not. Reasonable people disagree about whether it adds clarity or clutter. The variadic comma in C++ is not contested in that way: the comma form is unambiguously correct, and the no-comma form is an artifact. But the name still works because it captures the absurdity of having a debate at all.

The committee could have introduced this change without a memorable name and it would have landed in changelogs as a footnote. Instead, it has a label that makes the intent obvious, signals the historical background, and is light enough to repeat without grinding the conversation to a halt.

For a language that often struggles to communicate its direction to working programmers, that is worth something.

The change itself will affect almost no one’s existing codebase in a disruptive way. But it is one more step in the long process of making C++ mean what it says and say only what it means, which is a project that will probably never fully finish.

Was this interesting?