ZzetT ZzetT - 1 month ago 16
C++ Question

Builder Pattern with C++ templates

I have a highly configurable class with many template parameters like this:

template<bool OptionA = false, bool OptionB = false, bool OptionC = false, class T = Class1B>
class MyClass
{
}


Now, if I want to create the class type and I only want to set OptionB to true, I have to do the following:

MyClass<false, true>


Especially for many template arguments, this get cumbersome.

No, my question is, is there any example available to create a template based class type by using the builder pattern?

I'm looking for something like this:

class Builder
{
useOptionA();
useOptionB();
useOptionC();
useClass2B(); //instead of Class1B
create();
}


Finally a call to
Builder.useOptionB().useOptionC().useClass2B.create()
should return
MyClass<false, true, true, Class2B>
. Is this possible?

Edit: Added class to template parameter list.

Answer

As others have said, the easiest way to do what you want is with an enum instead of a Builder. If, however, you do want a Builder, you can try something like this:

template<bool OptionA = false,
         bool OptionB = false,
         bool OptionC = false,
         typename T = Class1B>
struct Builder_t
{
    Builder_t() = default;
//    ~Builder_t() { std::cout << "Builder dtor." << std::endl; }

    auto useOptionA() -> Builder_t<true, OptionB, OptionC, T>          { return {}; }
    auto useOptionB() -> Builder_t<OptionA, true, OptionC, T>          { return {}; }
    auto useOptionC() -> Builder_t<OptionA, OptionB, true, T>          { return {}; }
    auto useClass2B() -> Builder_t<OptionA, OptionB, OptionC, Class2B> { return {}; }

    MyClass<OptionA, OptionB, OptionC, T> create() { return {}; }
};
using Builder = Builder_t<>;

// ...

// Build MyClass<true, false, false, Class2B>:
auto ma2 = Builder{}.useOptionA().useClass2B().create();

This causes each function to return a distinct Builder, whose template will be used by the next function; the final template is used as MyClass' template. Each function modifies its specified template parameter, allowing a compile-time version of the Builder pattern. It does have a cost, though, which becomes apparent if the user-defined destructor is uncommented.


Consider this simple test program:

#include <iostream>
#include <typeinfo>

class Class1B {};
class Class2B {};

template<bool OptionA = false,
         bool OptionB = false,
         bool OptionC = false,
         typename T = Class1B>
class MyClass
{
  public:
    MyClass() {
        std::cout << "MyClass<"
                  << OptionA << ", "
                  << OptionB << ", "
                  << OptionC << ", "
                  << "type " << typeid(T).name() << ">"
                  << std::endl;
    }
};

template<bool OptionA = false,
         bool OptionB = false,
         bool OptionC = false,
         typename T = Class1B>
struct Builder_t
{
    Builder_t() = default;
//    ~Builder_t() { std::cout << "Builder dtor." << std::endl; }

    auto useOptionA() -> Builder_t<true, OptionB, OptionC, T>          { return {}; }
    auto useOptionB() -> Builder_t<OptionA, true, OptionC, T>          { return {}; }
    auto useOptionC() -> Builder_t<OptionA, OptionB, true, T>          { return {}; }
    auto useClass2B() -> Builder_t<OptionA, OptionB, OptionC, Class2B> { return {}; }

    MyClass<OptionA, OptionB, OptionC, T> create() { return {}; }
};
using Builder = Builder_t<>;

int main()
{
    std::cout << std::boolalpha;

    std::cout << "Default:\n";
    std::cout << "Direct:  ";
      MyClass<> m;
    std::cout << "Builder: ";
      auto mdefault = Builder{}.create();
    std::cout << std::endl;

    std::cout << "Builder pattern:\n";
    std::cout << "A: ";
      auto ma = Builder{}.useOptionA().create();
    std::cout << "C: ";
      auto mc = Builder{}.useOptionC().create();
    std::cout << "---\n";

    std::cout << "AB: ";
      auto mab = Builder{}.useOptionA().useOptionB().create();
    std::cout << "B2: ";
      auto mb2 = Builder{}.useOptionB().useClass2B().create();
    std::cout << "---\n";

    std::cout << "ABC: ";
      auto mabc = Builder{}.useOptionA().useOptionB().useOptionC().create();
    std::cout << "AC2: ";
      auto mac2 = Builder{}.useOptionA().useOptionC().useClass2B().create();
    std::cout << "---\n";

    std::cout << "ABC2: ";
      auto mabc2 = Builder{}.useOptionA().useOptionB().useOptionC().useClass2B().create();
}

Normally, the output is as follows (using GCC):

Default:
Direct:  MyClass<false, false, false, type 7Class1B>
Builder: MyClass<false, false, false, type 7Class1B>

Builder pattern:
A: MyClass<true, false, false, type 7Class1B>
C: MyClass<false, false, true, type 7Class1B>
---
AB: MyClass<true, true, false, type 7Class1B>
B2: MyClass<false, true, false, type 7Class2B>
---
ABC: MyClass<true, true, true, type 7Class1B>
AC2: MyClass<true, false, true, type 7Class2B>
---
ABC2: MyClass<true, true, true, type 7Class2B>

However, if we uncomment the destructor...

Default:
Direct:  MyClass<false, false, false, type 7Class1B>
Builder: MyClass<false, false, false, type 7Class1B>
Builder dtor.

Builder pattern:
A: MyClass<true, false, false, type 7Class1B>
Builder dtor.
Builder dtor.
C: MyClass<false, false, true, type 7Class1B>
Builder dtor.
Builder dtor.
---
AB: MyClass<true, true, false, type 7Class1B>
Builder dtor.
Builder dtor.
Builder dtor.
B2: MyClass<false, true, false, type 7Class2B>
Builder dtor.
Builder dtor.
Builder dtor.
---
ABC: MyClass<true, true, true, type 7Class1B>
Builder dtor.
Builder dtor.
Builder dtor.
Builder dtor.
AC2: MyClass<true, false, true, type 7Class2B>
Builder dtor.
Builder dtor.
Builder dtor.
Builder dtor.
---
ABC2: MyClass<true, true, true, type 7Class2B>
Builder dtor.
Builder dtor.
Builder dtor.
Builder dtor.
Builder dtor.

Each call preceding Builder_t::create() creates a distinct Builder_t, all of which are subsequently destroyed after the instance is created. This can be mitigated by making Builder_t a constexpr class, but that can potentially slow compilation if there are a large number of parameters to deal with:

template<bool OptionA = false,
         bool OptionB = false,
         bool OptionC = false,
         typename T = Class1B>
struct Builder_t
{
// Uncomment if you want to guarantee that your compiler treats Builder_t as constexpr.
//    size_t CompTimeTest;

    constexpr Builder_t()
// Uncomment if you want to guarantee that your compiler treats Builder_t as constexpr.
//      : CompTimeTest((OptionA ? 1 : 0) +
//                     (OptionB ? 2 : 0) +
//                     (OptionC ? 4 : 0) +
//                     (std::is_same<T, Class2B>{} ? 8 : 0))
    {}

    constexpr auto useOptionA() -> Builder_t<true, OptionB, OptionC, T>          { return {}; }
    constexpr auto useOptionB() -> Builder_t<OptionA, true, OptionC, T>          { return {}; }
    constexpr auto useOptionC() -> Builder_t<OptionA, OptionB, true, T>          { return {}; }
    constexpr auto useClass2B() -> Builder_t<OptionA, OptionB, OptionC, Class2B> { return {}; }

    constexpr MyClass<OptionA, OptionB, OptionC, T> create() { return {}; }
};
using Builder = Builder_t<>;

// ....

// Uncomment if you want to guarantee that your compiler treats Builder_t as constexpr.
// char arr[Builder{}/*.useOptionA()/*.useOptionB()/*.useOptionC()/*.useClass2B()/**/.CompTimeTest];
// std::cout << sizeof(arr) << '\n';