Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 64

Building Complex Objects with the Builder Pattern

Recommended resources

Hello! Welcome to your next lesson in C++ design patterns.

In our previous session, we implemented the Factory Method, a creational pattern that lets subclasses decide which object to instantiate. We saw how it uses runtime polymorphism (vtable/vptr) to decouple client code from concrete product types and how modern C++ uses std::unique_ptr to manage ownership safely.

Today, we'll explore another powerful creational pattern: the Builder. While the Factory Method is about creating one of several possible object types, the Builder pattern focuses on constructing a single, complex object step-by-step. This allows you to produce different representations of an object using the same construction process.

Your learning outcome for this lesson is to implement the Builder pattern to construct complex objects step-by-step, separating construction from its representation. We will start by identifying the problem it solves, analyze the classic "Gang of Four" implementation, and then explore more modern and idiomatic C++ approaches.

1. The Problem: Telescoping Constructors and Inconsistent State

Imagine you need to construct an object that has many configuration options, most of which are optional. A good example from your systems programming interest would be creating a network request object, which might have a URL (required) but also optional headers, a request body, a timeout, and various flags.

How would you approach this?

  1. The "Telescoping Constructor": You create a constructor with all possible arguments. This becomes unwieldy and error-prone. What if you want to set the first and last parameters but use defaults for those in between?
    // Hard to read, easy to mix up arguments of the same type
    HttpRequest("https://api.example.com", "POST", "application/json", "{...}", 5000, true, false);
    
  2. Multiple Constructors: You create overloaded constructors for common combinations. This leads to a combinatorial explosion of constructors that is impossible to maintain.
  3. Setters after Default Construction: You create a default-constructed object and then call setter methods. This is better, but it has two drawbacks:
    • The object exists in an intermediate, possibly incomplete state between its construction and the final setter call.
    • It prevents you from making the object immutable (e.g., by making members const).

C++ Builder design pattern: A pragmatic approach

This video provides a great, pragmatic overview of the problem. It uses the example of creating a UI menu with various optional attributes. Pay close attention to the drawbacks of each traditional approach he discusses before introducing the Builder.

Watch from 00:40 to 02:36 and then from 04:38 to 06:59. The first segment introduces the core problem of complex object initialization. The second segment applies this problem to a concrete Menu example and critiques the non-Builder solutions.

The Builder pattern is designed to solve exactly this problem by providing a flexible and readable way to construct complex objects.


2. The Classic Builder Pattern: Separating Construction and Representation

The classic design pattern, as described by the "Gang of Four," separates the construction process into four distinct roles:

  • Product: The complex object being built.
  • Builder: An abstract interface for creating parts of the Product object.
  • ConcreteBuilder: A class that implements the Builder interface, constructing and assembling the parts of the product. It keeps track of the representation it creates and provides a way to retrieve the final product.
  • Director: A class that constructs an object using the Builder interface. It encapsulates the specific sequence of steps to build a particular representation of the product. The client only needs to know about the Director and the abstract Builder.

This structure allows the same construction process (the Director) to create different representations (by using different ConcreteBuilders).

UML Class Diagram for Builder Design Pattern
This UML diagram shows the relationships between the four components. The Director holds a reference to a Builder. The client configures the Director with a specific ConcreteBuilder. The Director then orchestrates the build process by calling methods on the Builder interface, and the ConcreteBuilder assembles the final Product.

Builder Design Pattern explained in Detail with Code, Real Life Examples and Applications!

Now, let's watch a detailed walkthrough of this classic structure implemented in C++. The example is building a desktop computer, which serves as an excellent 'complex object.' Notice how each of the four roles is mapped to a specific class.

Watch from 04:20 to 23:06. This covers the implementation of all four components and how the client uses them: 04:20 - 07:17: Introduction to the four components (Product, Builder, ConcreteBuilder, Director). 07:17 - 09:16: Implementing the Desktop (Product). 09:16 - 12:42: Implementing the DesktopBuilder interface. 12:42 - 14:43: Implementing DellDesktopBuilder and HPDesktopBuilder (ConcreteBuilders). 15:36 - 20:02: Implementing the DesktopDirector. 20:02 - 23:06: How the client code puts it all together.

Key Analysis Points

  • Separation of Concerns: The Director knows how to build (the algorithm, the steps), while the ConcreteBuilder knows what to build (the specific parts for a Dell vs. HP machine). The client is completely isolated from the complex construction logic.
  • Optional Director: As the video mentions, the Director class is optional. A client can call the building steps on the builder instance directly if it needs more control over the construction process.
  • Memory Management: The video uses raw pointers (new). As we learned in the last lesson, this is problematic. A modern implementation would have the builder's final getDesktop() method (or build() method) return a std::unique_ptr<Desktop>, clearly transferring ownership to the client.

3. The Fluent Builder: An Idiomatic C++ Approach

While the classic pattern is powerful, its verbosity (separate classes for Director, Builder, etc.) isn't always necessary in C++. A more common and idiomatic approach is the fluent builder, which uses method chaining.

Each setter method in the builder returns a reference to itself (*this), allowing you to chain calls together in a single, highly readable statement.

// Example of a fluent interface
EmailBuilder builder;
builder.from("tutor@ai.com")
       .to("learner@cpp.com")
       .subject("Builder Pattern Lesson")
       .body("Here is your lesson content...");

Let's look at how to implement this.

C++ Builder design pattern: A pragmatic approach

Let's return to the first video, which demonstrates two pragmatic fluent builder implementations. The first is a very simple approach, and the second is the more traditional separate-class approach, but both use method chaining.

Watch the section from 06:59 to 12:19. The first part demonstrates a simple fluent interface directly on the product class. The second, more robust implementation shows a separate MenuBuilder class that constructs a MenuImpl object, returning it only when build() is called.

The second variant shown in the video is generally preferred:

  • A separate Builder class holds an instance of the Product being built.
  • Setter methods (.setTitle(...), .setOptions(...)) modify the internal product instance and return *this.
  • A final build() method returns the completed Product object, often by value or, even better, by moving it out of the builder.

Modern C++ Enhancements

Modern C++ gives us tools to make this even more concise and powerful.

C++20 Designated Initializers: For simple aggregate types (structs with no private members, virtual functions, or user-defined constructors), you might not need a builder at all! C++20 designated initializers provide a "named parameter" feel.

struct HttpConfig {
    std::string method = "GET";
    int timeout_ms = 2000;
    bool follow_redirects = true;
};

// C++20: Clear, readable, no builder needed!
HttpConfig config { .timeout_ms = 5000, .follow_redirects = false };

C++17 Class Template Argument Deduction (CTAD): For non-aggregates, we can create a generic, fluent builder that's incredibly clean.

Modern C++23 Design Patterns: Fluent Builder

This article discusses modern C++ patterns and has an excellent section on a fluent builder that leverages CTAD. It's a great example of a minimal yet powerful implementation.

Read the section titled 'Fluent Builder (with Designated Init and CTAD)'. Pay close attention to the Builder template and how Builder{Rectangle{}} automatically deduces the template argument T as Rectangle. Also, note the clever use of a conversion operator (operator T()) to avoid an explicit .build() call.

This modern approach is powerful because:

  1. CTAD deduces the type, so you don't write Builder<Rectangle>.
  2. The conversion operator operator T() && allows the builder to be implicitly converted to the product type at the end of the chain, making the final code auto rect = Builder{...}.set...;. This is a very C++-specific technique that leverages move semantics (the && on the operator).
Test your understanding!

You need to construct an SQL query string. A query has required SELECT and FROM clauses, and optional WHERE, GROUP BY, and ORDER BY clauses.

Sketch out a C++ fluent builder class SQLQueryBuilder for this. What would the key methods be? How would you handle the final query string generation?

Show answer

You would create a class SQLQueryBuilder that stores the different parts of the query as std::string.

class SQLQueryBuilder {
private:
    std::string select_clause;
    std::string from_clause;
    std::string where_clause;
    // ... other clauses

public:
    // The constructor takes the required parts.
    SQLQueryBuilder(std::string select, std::string from)
        : select_clause(std::move(select)), from_clause(std::move(from)) {}

    // Methods for optional parts return a reference to self for chaining.
    SQLQueryBuilder& where(std::string conditions) {
        where_clause = std::move(conditions);
        return *this;
    }

    SQLQueryBuilder& orderBy(std::string column) {
        // ... implementation ...
        return *this;
    }

    // A final `build` method assembles the string.
    std::string build() const {
        std::string query = "SELECT " + select_clause + " FROM " + from_clause;
        if (!where_clause.empty()) {
            query += " WHERE " + where_clause;
        }
        // ... append other clauses ...
        query += ";";
        return query;
    }
};

// Usage:
SQLQueryBuilder builder("id, name", "Users");
std::string query = builder.where("age > 30").orderBy("name").build();

4. Advanced Topic: Builders and Inheritance

Here's a problem that digs into C++'s type system, perfect for an "under the hood" understanding. What happens if your builders need to inherit from each other?

Imagine a VehicleBuilder with a setWheels(int) method and a CarBuilder that inherits from it and adds a setDoors(int) method.

CarBuilder builder;
auto& result = builder.setWheels(4); // setWheels returns VehicleBuilder&
result.setDoors(4);                  // ERROR! VehicleBuilder has no setDoors method.

The chain is broken! The base class method returns a reference to the base class, losing the derived type information.

How do we solve this? We can use a compile-time polymorphism technique called the Curiously Recurring Template Pattern (CRTP).

Inheritance with Builder pattern - c++

This Stack Overflow question and its top answer provide an excellent, deep dive into solving the builder inheritance problem with CRTP. This is a classic C++ technique.

First, read the question to fully understand the problem with chaining and inheritance (sections 0 & 1). Then, read the first part of the accepted answer (section 3), which introduces the CRTP-based solution. The key is how the base class can static_cast itself to the derived type, which is known at compile time.

In the CRTP solution, the base class is templated on the derived class type. This allows the base class methods to safely static_cast the this pointer to the derived type and return a Derived&, thus preserving the full type information and allowing the method chain to continue.

// Base builder uses CRTP
template <typename Derived>
class VehicleBuilder {
public:
    Derived& setWheels(int count) {
        // ... logic ...
        return static_cast<Derived&>(*this); // Return the actual derived type
    }
};

// Derived builder inherits from the templated base
class CarBuilder : public VehicleBuilder<CarBuilder> {
public:
    CarBuilder& setDoors(int count) {
        // ... logic ...
        return *this;
    }
};

// Now the chain works!
CarBuilder().setWheels(4).setDoors(4);

This is a perfect example of leveraging C++'s powerful template system to solve a design pattern problem elegantly at compile time, avoiding any runtime overhead.

Test your understanding!

In the CRTP solution for the builder inheritance problem, the base class uses static_cast<Derived&>(*this). Why is static_cast used here instead of dynamic_cast? What does this choice imply about when the type conversion is checked?

Show answer

static_cast is used because the relationship between the base class VehicleBuilder<CarBuilder> and the derived class CarBuilder is known at compile time. CRTP is a compile-time pattern. The compiler knows that an object of type VehicleBuilder<CarBuilder> will always be a base part of a CarBuilder object.

dynamic_cast, on the other hand, is used for runtime polymorphism. It performs a type check at runtime to see if a downcast is valid, which requires the classes to have virtual functions (and a vtable). This would incur a runtime performance cost and is unnecessary here, as the type information is already fully available to the compiler.

The use of static_cast implies that the type check is done at compile time, resulting in zero runtime overhead for the cast itself.


Conclusion

Today we've deconstructed the Builder pattern, from the problem it solves to its classic implementation and modern C++ idioms. It's a fundamental pattern for creating complex objects in a readable and maintainable way.

Key Takeaways:

  • The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations.
  • It solves the telescoping constructor problem and avoids leaving objects in an incomplete state.
  • The classic implementation uses four components: Director, Builder, ConcreteBuilder, and Product.
  • A fluent interface with method chaining is a common and highly readable idiom in C++.
  • Modern C++ offers alternatives like designated initializers for simple aggregates and enables concise fluent builders using features like CTAD and conversion operators.
  • Advanced C++ techniques like CRTP can be used to effectively solve complex issues like maintaining method chains in builder inheritance hierarchies.

In our next lesson, we will move from creational patterns to structural patterns. We'll begin with the Adapter pattern, which focuses on making incompatible interfaces work together—a very common problem in real-world software integration.