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

  • 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();
  • &&-qualified build() rejects Builder 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 Product exposes setters anyway, Builder adds ceremony with no benefit. Builder shines when Product is immutable.
  • build() left callable many times — caller calls build(), then keeps mutating the builder, then calls build() again → confusion. Use &&-qualified build() to make it one-shot.
  • Builder leaks shared stateaddTopping pushes into a vector; calling build() 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 *this on 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