Factories and builders

A constructor can only build one type, and it cannot fail without an exception or a flag. A factory is a function that decides which type to build and how, and a builder is a class that collects the arguments over several calls before it builds anything.

Both exist to keep the construction in one place. The factory hides the derived types behind a base pointer, and the builder turns a long parameter list into named steps that can be read and checked.

The builder can also be a factory's argument: the factory takes the finished configuration and decides the type. The two patterns compose, and the composition is what most real creation code looks like.

A builder is also the natural place to attach a name to each step for a command line: the parser maps a word to a method, and the builder keeps the state the words collected.

A factory function

make_shape takes a name and two numbers and returns a std::unique_ptr<shape>. The caller never writes new and never names circle or square, and the ownership rule of lesson 091 applies to the result.

The program builds two shapes by name and prints their areas: 4 for the square and 3 for the circle, truncated. An unknown name returns nullptr, which the caller checks.

The factory is the place to add a type: a new shape touches the factory and nothing else. A factory that can fail without a null pointer returns std::optional or std::expected, which lesson 145 uses for the same decision.

#include <iostream> #include <memory> #include <string> class shape { public: virtual ~shape() = default; virtual double area() const = 0; }; class circle : public shape { public: explicit circle(double radius) : radius_(radius) {} double area() const override { return 3.14159 * radius_ * radius_; } private: double radius_; }; class square : public shape { public: explicit square(double side) : side_(side) {} double area() const override { return side_ * side_; } private: double side_; }; std::unique_ptr<shape> make_shape(const std::string& name, double size) { if (name == "circle") { return std::make_unique<circle>(size); } if (name == "square") { return std::make_unique<square>(size); } return nullptr; } int main() { auto first = make_shape("square", 2.0); auto second = make_shape("circle", 1.0); auto missing = make_shape("triangle", 1.0); std::cout << static_cast<int>(first->area()) << ' '; std::cout << static_cast<int>(second->area()) << ' '; std::cout << (missing == nullptr) << '\n'; }

g++ -std=c++23 -Wall -Wextra factory.cpp -o factory_shapes
./factory_shapes

4 3 1

A builder with named steps

A builder stores the parts of an object in members and returns *this from each step, so the calls chain. build is the only place that constructs the result, and it can validate the state that the steps collected.

The program builds a coffee with two sugars and milk and prints its description and price. The steps read in the order of the problem, and a default value covers what the caller did not set.

A constructor with ten parameters is the problem the builder solves: the call site says nothing about which bool is which. A builder trades one class for a readable sequence, and the trade pays when the object has many optional parts.

#include <iostream> #include <string> class coffee { public: coffee(std::string size, int sugars, bool milk) : size_(std::move(size)), sugars_(sugars), milk_(milk) {} std::string describe() const { return size_ + ", " + std::to_string(sugars_) + " sugars, milk: " + (milk_ ? "yes" : "no"); } int price() const { return (size_ == "large" ? 3 : 2) + sugars_ / 2 + (milk_ ? 1 : 0); } private: std::string size_; int sugars_; bool milk_; }; class coffee_builder { public: coffee_builder& size(std::string value) { size_ = std::move(value); return *this; } coffee_builder& sugars(int value) { sugars_ = value; return *this; } coffee_builder& with_milk() { milk_ = true; return *this; } coffee build() const { return coffee{size_, sugars_, milk_}; } private: std::string size_ = "medium"; int sugars_ = 0; bool milk_ = false; }; int main() { coffee drink = coffee_builder{}.size("large").sugars(2).with_milk().build(); std::cout << drink.describe() << '\n'; std::cout << drink.price() << '\n'; }

g++ -std=c++23 -Wall -Wextra builder.cpp -o builder_coffee
./builder_coffee

large, 2 sugars, milk: yes
5

An object that is valid when it is built

The builder's build can refuse. The program validates a port number and returns std::optional<server_config>, so the caller cannot get an object with an impossible port.

The builder stores the port and the name, checks the range in build and returns an empty optional for 70000. The print is 8080 and 0 for the invalid attempt.

The alternative is a constructor that throws, which is right when an invalid object is a programming error. An optional or expected is right when the values come from a file or a user, and the caller has to handle the refusal without an exception.

#include <iostream> #include <optional> #include <string> class server_config { public: server_config(int port, std::string name) : port_(port), name_(std::move(name)) {} int port() const { return port_; } const std::string& name() const { return name_; } private: int port_; std::string name_; }; class config_builder { public: config_builder& port(int value) { port_ = value; return *this; } config_builder& name(std::string value) { name_ = std::move(value); return *this; } std::optional<server_config> build() const { if (port_ <= 0 || port_ > 65535) { return std::nullopt; } return server_config{port_, name_}; } private: int port_ = 0; std::string name_ = "server"; }; int main() { auto good = config_builder{}.port(8080).name("web").build(); auto bad = config_builder{}.port(70000).build(); std::cout << good->port() << ' ' << good->name() << ' '; std::cout << bad.has_value() << '\n'; }

g++ -std=c++23 -Wall -Wextra valid.cpp -o builder_valid
./builder_valid

8080 web 0

The constructor that is private

A factory often makes the constructor private, so that every object comes through the factory. Code outside then cannot create one directly, and the compiler says the constructor is private in this context.

The program declares a private constructor and tries to build the object in main. The message names the class and the constructor, and the note points at the private declaration.

The fix is to call the factory, or to make the constructor public when direct construction is part of the interface. A private constructor is a promise that the class controls its own creation, and the error is that promise working.

class token { public: static token create() { return token{42}; } private: explicit token(int value) : value_(value) {} int value_; }; int main() { token direct{42}; }

g++ -std=c++23 -Wall -Wextra private.cpp -o private_ctor

private.cpp: In function 'int main()':
private.cpp:14:20: error: 'token::token(int)' is private within this context
14 | token direct{42};
| ^
private.cpp:7:14: note: declared private here
7 | explicit token(int value) : value_(value) {}
| ^~~~~

Exercises

  1. Add a rectangle to the factory and a name for it.
  2. Give the coffee builder a step for a second shot and print the new price.
  3. Make build return std::expected with an error message instead of std::optional.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does a factory function return when the caller must not know the derived type?

    Answer

    A pointer to the base class

    The base pointer carries the virtual interface and hides which derived type was built.

  2. Why does each builder step return *this?

    Answer

    So the calls chain in one expression

    The reference back to the builder is what allows builder.size(...).sugars(...).build().

  3. What is the advantage of a builder over a constructor with many parameters?

    Answer

    The call site names each value and the optional parts can be left out

    Named steps replace a long positional list, and defaults cover what the caller does not set.

  4. What does this program print? #include <iostream> #include <memory> #include <string> struct shape { virtual ~shape() = default; virtual double area() const = 0; }; struct circle : shape { explicit circle(double r) : radius(r) {} double area() const override { return 3.14159 * radius * radius; } double radius; }; struct square : shape { explicit square(double s) : side(s) {} double area() const override { return side * side; } double side; }; std::unique_ptr<shape> make_shape(const std::string& name, double size) { if (name == "circle") return std::make_unique<circle>(size); if (name == "square") return std::make_unique<square>(size); return nullptr; } int main() { auto a = make_shape("square", 2.0); auto b = make_shape("circle", 1.0); auto c = make_shape("triangle", 1.0); std::cout << static_cast<int>(a->area()) << ' ' << static_cast<int>(b->area()) << ' ' << (c == nullptr); }

    Answer

    4 3 1

    The square is 4, the circle is 3.14 truncated to 3, and the unknown name returns nullptr.

  5. What does this program print? #include <iostream> #include <string> class coffee { public: coffee(std::string size, int sugars, bool milk) : size_(size), sugars_(sugars), milk_(milk) {} std::string describe() const { return size_ + ", " + std::to_string(sugars_) + " sugars, milk: " + (milk_ ? "yes" : "no"); } int price() const { return (size_ == "large" ? 3 : 2) + sugars_ / 2 + (milk_ ? 1 : 0); } private: std::string size_; int sugars_; bool milk_; }; int main() { coffee drink{"large", 2, true}; std::cout << drink.describe() << '\n' << drink.price(); }

    Answer

    large, 2 sugars, milk: yes / 5

    The fields build the description, and the price is 3 + 1 + 1.

  6. What does the validating builder print for the good and the bad config?

    Answer

    8080 web 0

    The good build returns the config, and the invalid port returns an empty optional.

  7. When is a throwing constructor better than an optional result?

    Answer

    When an invalid object is a programming error, not user input

    An exception is for a bug that must stop the program; input from a file or a user needs a value the caller checks.

  8. The build stops at this line. What does the compiler report? class token { public: static token create(); private: explicit token(int value); }; token direct{42};

    Answer

    'token::token(int)' is private within this context

    Only the class and its friends can call a private constructor, and this line is outside.

  9. Which return type lets a factory report an error without a null pointer?

    Answer

    std::expected

    expected carries either the object or an error value, and the caller has to handle both.

  10. What does the factory hide from the caller?

    Answer

    Which derived type is built and how it is allocated

    The caller sees a base pointer and calls virtual functions; the construction stays in one function.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev