Variadic Programming in C and C++

Last update: 2022-09-05 18:24:42 +0800 +0800
Available translations: 日本語 / 中文(简体) / 中文(繁體)

C++ variadic templates preserve the type and count of every argument. C ellipsis functions erase that information, which shifts type interpretation and argument-count discipline to the caller. Prefer C++ packs for new typed interfaces.

Packs, Expansions, and Folds #

A template parameter pack and a function parameter pack may contain zero or more elements. A pack expansion applies a pattern once per element.

cpp
#include <utility>

template <class Function, class... Ts>
decltype(auto) invoke(Function&& function, Ts&&... values) {
    return std::forward<Function>(function)(std::forward<Ts>(values)...);
}

template <class... Ts>
auto sum(Ts... values) {
    return (0 + ... + values); // binary left fold; 0 is the empty-pack identity
}

std::forward preserves the value category deduced for each forwarding reference. It does not extend an argument’s lifetime, so a callee must still obey its ordinary reference and ownership contract.

Expansions occur in template argument lists, function arguments, base-specifier lists, initializers, lambda captures, and other grammar-defined contexts. Nested or simultaneous expansions require packs with compatible lengths.

A unary fold, such as (values + ...), requires a nonempty pack unless the operator has a language-defined empty identity. Supply an explicit identity with a binary fold when empty input is meaningful; reject empty input with a constraint when it is not.

cpp
template <class... Ts>
requires (sizeof...(Ts) > 0)
auto product(Ts... values) {
    return (... * values);
}

C Variadic Functions #

A C function declared with ... reads unnamed arguments through va_list, va_start, va_arg, va_copy, and va_end. The declaration supplies neither count nor type metadata. A count parameter, format string, or documented sentinel is therefore part of the API contract.

c
#include <stddef.h>
#include <stdarg.h>

int sum_ints(size_t count, ...) {
    va_list arguments;
    va_start(arguments, count);
    int result = 0;
    for (size_t i = 0; i < count; ++i)
        result += va_arg(arguments, int);
    va_end(arguments);
    return result;
}

Default argument promotions apply across ...: float becomes double, and integer types narrower than int promote to int or unsigned int. va_arg must request the promoted, compatible type expected at that position. Reading the wrong type, omitting va_end, reading past the supplied arguments, or relying on an absent sentinel has undefined behavior. Use C varargs only for ABI compatibility or interfaces such as formatting that already impose a precise convention.

Comments

Read or comment on GitHub (sign-in required to comment)