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++98A 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:
// 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.
#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.
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:
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.
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 callingbuild()twice on the same builder object and signals intent clearly. - Return
*thisby reference, not value — returning by value in a setter copies the entire builder state on each call. Return*thisor move*thisinto 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 thanServer::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