Skip to content
C++
Idiom
since C++98
Basic

Named Parameter Idiom

Simulate named function arguments in C++ using method-chaining builders, options structs, and C++20 designated initializers for self-documenting call sites.

Named Parameter Idiomsince C++98

A technique that simulates named function arguments by expressing configuration through a builder's chainable setters or an aggregate struct's fields, making call sites self-documenting and resistant to argument-order bugs.

Overview

C++ passes arguments positionally. When a function accepts more than two or three parameters — especially booleans or integers — the call site becomes opaque:

cpp
// What does each argument mean?
auto conn = connect("db.host", 5432, true, false, 30, 10, true);

No amount of IDE tooltips fully solves this at code review time. The named parameter idiom attacks the problem at the API level, with two main strategies:

  • Builder (method chaining) — a dedicated object accumulates configuration through named setters, then constructs the target. Natural for objects requiring validation or invariant enforcement. C++11 move semantics make this efficient.
  • Options struct — a plain aggregate holds configuration with defaults. C++20 designated initializers give it syntax nearly identical to true named parameters.

Choose the options struct for simple configuration bags. Reach for a builder when validation is non-trivial, construction order matters, or the result should be immutable.

Examples

Builder with Method Chaining

Each setter returns *this (C++11). Declare members with defaults so unset fields don't need explicit initialization.

cpp
#include <chrono>
#include <string>
#include <unordered_map>

class HttpRequest {
public:
    HttpRequest& url(std::string u) {
        url_ = std::move(u);    // C++11: move instead of copy
        return *this;
    }
    HttpRequest& method(std::string m) {
        method_ = std::move(m);
        return *this;
    }
    HttpRequest& header(std::string key, std::string val) {
        headers_.emplace(std::move(key), std::move(val));  // C++11
        return *this;
    }
    HttpRequest& timeout(std::chrono::milliseconds t) {    // C++11: chrono
        timeout_ = t;
        return *this;
    }

    Response send();   // executes the request

private:
    std::string url_;
    std::string method_ = "GET";
    std::unordered_map<std::string, std::string> headers_;  // C++11
    std::chrono::milliseconds timeout_{5'000};              // C++14: digit separator
};

// Self-documenting at the call site
auto response = HttpRequest{}
    .url("https://api.example.com/users")
    .method("POST")
    .header("Content-Type", "application/json")
    .header("Authorization", "Bearer " + token)
    .timeout(std::chrono::seconds{10})  // C++11: duration literals
    .send();

Separate Builder for Immutable Objects

When the constructed object should be immutable post-construction, separate the builder from the product. Mark build() as && (ref-qualifier, C++11) so it can only be called on a temporary or explicitly moved builder — preventing accidental reuse.

cpp
class TlsConfig {
public:
    class Builder {
    public:
        Builder& cert_file(std::string path) {
            cert_ = std::move(path);
            return *this;
        }
        Builder& key_file(std::string path) {
            key_ = std::move(path);
            return *this;
        }
        Builder& ca_bundle(std::string path) {
            ca_ = std::move(path);
            return *this;
        }
        Builder& verify_peer(bool v) {
            verify_ = v;
            return *this;
        }

        // rvalue ref-qualifier (C++11): builder is consumed, no second build()
        TlsConfig build() && {
            if (cert_.empty() || key_.empty())
                throw std::invalid_argument{"cert and key are required"};
            return TlsConfig{std::move(cert_), std::move(key_),
                             std::move(ca_), verify_};
        }

    private:
        std::string cert_, key_, ca_;
        bool verify_ = true;
    };

private:
    TlsConfig(std::string c, std::string k, std::string ca, bool v)
        : cert_(std::move(c)), key_(std::move(k)), ca_(std::move(ca)), verify_(v) {}

    const std::string cert_, key_, ca_;
    const bool verify_;
};

auto tls = TlsConfig::Builder{}
    .cert_file("/etc/ssl/server.crt")
    .key_file("/etc/ssl/server.key")
    .ca_bundle("/etc/ssl/ca-bundle.crt")
    .build();

Options Struct with Designated Initializers (C++20)

For configuration without complex validation, an aggregate struct with defaults is less ceremony than a builder:

cpp
struct ConnectOptions {
    std::string host          = "localhost";
    uint16_t    port          = 5432;
    bool        ssl           = false;
    int         connect_timeout_ms = 5'000;   // C++14: digit separators
    int         pool_size     = 10;
    std::string user          = "postgres";
    std::string password;
    std::string application_name;
};

std::shared_ptr<DB> connect(ConnectOptions opts = {});

// C++20: designated initializers — members named at the call site
// Unspecified fields get their struct defaults.
auto db = connect({
    .host          = "db.prod.internal",
    .ssl           = true,
    .pool_size     = 50,
    .user          = "app",
    .password      = secret,
    .application_name = "payments-service",
});

Constraint: C++20 designated initializers must appear in the same order as the struct's member declarations. Reordering designators is ill-formed. Pre-C++20, aggregate initialization works without field names — rely on defaults and document the struct carefully.

Type-State Builder: Required Parameters at Compile Time

A template-based type-state builder uses a bitfield template parameter to track which required fields have been set. build() is only enabled when all required bits are present.

cpp
template<unsigned Flags = 0>
class ServerBuilder {
    static constexpr unsigned kHost = 1u << 0;
    static constexpr unsigned kPort = 1u << 1;
    static constexpr unsigned kAll  = kHost | kPort;

    std::string host_;
    int port_        = 0;
    bool ssl_        = false;
    int max_conn_    = 128;

    template<unsigned F> friend class ServerBuilder;

    // Private: copy state into a new builder with updated Flags
    template<unsigned NewFlags>
    ServerBuilder<NewFlags> rebrand() && {
        ServerBuilder<NewFlags> next;
        next.host_     = std::move(host_);
        next.port_     = port_;
        next.ssl_      = ssl_;
        next.max_conn_ = max_conn_;
        return next;
    }

public:
    [[nodiscard]] ServerBuilder<Flags | kHost> host(std::string h) && {
        host_ = std::move(h);
        return std::move(*this).template rebrand<Flags | kHost>();
    }
    [[nodiscard]] ServerBuilder<Flags | kPort> port(int p) && {
        port_ = p;
        return std::move(*this).template rebrand<Flags | kPort>();
    }
    ServerBuilder& ssl(bool s)       & { ssl_      = s; return *this; }
    ServerBuilder& max_connections(int n) & { max_conn_ = n; return *this; }

    // C++20 requires: only callable when host and port are set
    Server build() && requires ((Flags & kAll) == kAll) {
        return Server{std::move(host_), port_, ssl_, max_conn_};
    }
};

// Compile error if .host() or .port() is missing — not a runtime error
auto s = ServerBuilder<>{}
    .host("api.example.com")
    .port(443)
    .ssl(true)
    .build();

The requires clause (C++20) makes build() disappear from the overload set entirely when preconditions aren't met, producing a clear diagnostic rather than a runtime exception.

Best Practices

  • Prefer options structs for pure configuration — they have lower overhead and C++20 designated initializers give the same readability as a builder.
  • Use a builder when construction has invariants — validate in build(), not in individual setters. Deferring validation means you see the complete picture before throwing.
  • Mark build() as && — prevents calling build() twice on the same builder object and signals intent clearly.
  • Return *this by reference, not value — returning by value in a setter copies the entire builder state on each call. Return *this or move *this into a new type (type-state pattern only).
  • Tag setter return types with [[nodiscard]] for type-state builders — discarding an intermediate builder silently loses required-field tracking.
  • Give the builder a static create() factory when the target type's name is long — Server::Builder::create().host(...).build() reads better than Server::Builder{}.host(...).build().

Common Pitfalls

Forgetting return *this — a setter that returns void silently breaks the chain. The compiler won't warn; the first chained call simply fails to compile with a confusing message.

Builder aliasing and reuse — storing a builder in a variable and calling setters on it is fine for the single-type (non-type-state) builder, but re-calling build() constructs a second object from potentially moved-from state. Mark build() as && to catch this.

Designated initializer ordering — C++20 requires designators in declaration order. Swapping .port before .host in the initializer when host is declared first is ill-formed. Some compilers emit only a warning; treat it as an error.

Boolean setters — avoid builder.ssl(true). Prefer builder.enable_ssl() / builder.disable_ssl() or an enum. A boolean setter is only marginally better than the positional boolean it replaces.

Options struct as mutable config — pass ConnectOptions by value into the consuming function. Callers holding a reference to an options struct and modifying it after the call is a maintenance trap.

See Also

  • std::initializer_list — aggregate initialization mechanics underlying options structs
  • CRTP — use CRTP to avoid repeating setter boilerplate in builder hierarchies
  • Policy-Based Design — compile-time parameterization complementary to runtime builders