Builder Pattern
- Description: Construct a complex object step by step, separating what to build from how to assemble — covers fluent interface with
std::move, classic GoF director form, named-parameter idiom, and comparison with Abstract Factory. - My Notion Note ID: K2C-2-4
- Created: 2026-05-22
- Updated: 2026-05-22
- License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io
Table of Contents
- 1. The Problem
- 2. Structure
- 3. C++ Implementations
- 4. When to Use, When Not To
- 5. Pitfalls
- 6. Related Patterns
- 7. References
1. The Problem
- Constructor with many parameters, several optional, several with sensible defaults → "telescoping constructors" (8 overloads, each adding one arg).
- C++ has no named arguments →
Pizza("medium", true, false, true, 3, ...)is unreadable at call sites. - Two-phase init (
Pizza p; p.setSize(...); p.setCheese(...); use(p);) → object exists in invalid intermediate state, error-prone. - Builder lets the caller specify only what matters, in any order, and produces a fully-formed object in one final step.
2. Structure
| Role | Responsibility |
|---|---|
Product |
The complex object being built. Often immutable post-build. |
Builder |
Accepts step calls (withSize, addTopping); accumulates state; exposes build(). |
Director (optional, GoF) |
Orchestrates known build sequences (e.g. buildMargherita()). |
- GoF defines the Director role. Modern C++ Builder usually drops the Director — the calling code itself is the director, expressed inline as a fluent chain.
3. C++ Implementations
3.1 Fluent builder with std::move (preferred modern form)
#include <string>
#include <vector>
#include <utility>
class Pizza {
public:
class Builder;
const std::string& size() const { return size_; }
bool cheese() const { return cheese_; }
const std::vector<std::string>& toppings() const { return toppings_; }
private:
Pizza() = default;
std::string size_{"medium"};
bool cheese_{true};
std::vector<std::string> toppings_;
friend class Builder;
};
class Pizza::Builder {
public:
Builder& withSize(std::string s) & { p_.size_ = std::move(s); return *this; }
Builder&& withSize(std::string s) && { p_.size_ = std::move(s); return std::move(*this); }
Builder& cheese(bool b) & { p_.cheese_ = b; return *this; }
Builder&& cheese(bool b) && { p_.cheese_ = b; return std::move(*this); }
Builder& addTopping(std::string t) & {
p_.toppings_.push_back(std::move(t));
return *this;
}
Builder&& addTopping(std::string t) && {
p_.toppings_.push_back(std::move(t));
return std::move(*this);
}
Pizza build() && { return std::move(p_); }
private:
Pizza p_;
};
// Usage — rvalue chain, no intermediate variables.
Pizza pie = Pizza::Builder{}
.withSize("large")
.cheese(true)
.addTopping("mushroom")
.addTopping("olive")
.build();
&&-qualifiedbuild()rejectsBuilder b; b.build();(b is an lvalue) → catches "forgot to chain" mistakes at compile time when intentional.
3.2 Classic GoF with Director
struct PizzaDirector {
static Pizza margherita() {
return Pizza::Builder{}
.withSize("medium")
.cheese(true)
.addTopping("basil")
.addTopping("tomato")
.build();
}
static Pizza hawaiian() {
return Pizza::Builder{}
.withSize("large")
.cheese(true)
.addTopping("ham")
.addTopping("pineapple")
.build();
}
};
- Director isolates change — recipe edits stay in one place.
3.3 Named-parameter idiom (lightweight builder)
struct PizzaParams {
std::string size = "medium";
bool cheese = true;
std::vector<std::string> toppings{};
};
class Pizza2 {
public:
explicit Pizza2(PizzaParams p) : p_{std::move(p)} {}
private:
PizzaParams p_;
};
// C++20 designated initializers — close to named arguments.
Pizza2 p{{.size = "large", .cheese = false, .toppings = {"olive"}}};
- No fluent chain, no Builder class, no
build(). Trade-off: no per-step validation, no incremental construction; all fields specified at the call site.
3.4 Generic / template builder
template <class Product>
class GenericBuilder {
public:
template <class F>
GenericBuilder& apply(F&& step) { step(p_); return *this; }
Product build() && { return std::move(p_); }
private:
Product p_{};
};
4. When to Use, When Not To
Use when:
- Constructor would take 5+ parameters, several optional.
- The object must be valid after construction (no two-phase init).
- Multiple construction recipes exist (Director form).
- You want fluent, readable call sites.
Avoid when:
- Object has 2–3 trivial params — direct ctor wins.
- C++20 designated initializers cover the case → § 3.3 is shorter.
5. Pitfalls
- Mutable Product — if
Productexposes setters anyway, Builder adds ceremony with no benefit. Builder shines when Product is immutable. build()left callable many times — caller callsbuild(), then keeps mutating the builder, then callsbuild()again → confusion. Use&&-qualifiedbuild()to make it one-shot.- Builder leaks shared state —
addToppingpushes into avector; callingbuild()twice on the same builder shares no state because we moved out — but a sloppy implementation that copies leaves both products aliasing the same data. - No validation gate — fluent chain accepts anything in any order. If certain combos are illegal, validate in
build(), not per-step (errors then point at the call site, not deep in the chain).
6. Related Patterns
- Builder vs Factory Method / Abstract Factory — Factories return finished products in one call. Builder accumulates state across calls and returns at
build(). Use Builder when construction itself is the complex part. - Builder vs Prototype — Prototype clones an existing instance, then optionally tweaks. Builder constructs from nothing. Prototype faster when configuration is expensive; Builder clearer when each instance is different.
- Builder vs Fluent Setters — Fluent setters mutate
*thison the product. Builder mutates a separate builder, then emits the product. Builder protects Product's invariants. - Builder vs Named-Parameter Idiom (§ 3.3) — Named params = struct of fields. Builder = stateful chain. Named params shorter; Builder allows incremental construction + validation.
- Builder vs Composite — Builder often used to assemble a Composite (e.g. a parse tree). Builder owns "how to assemble"; Composite is the shape of the result.
7. References
- Builder — refactoring.guru
- Builder — sourcemaking
- GoF, Design Patterns, ch. 3 — Builder (p. 97)
- cppreference — Designated initializers (C++20)
- Effective Java, Item 2 — "Consider a builder when faced with many constructor parameters"