Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Builder Pattern with C++ templates

Tags:

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.

like image 387
ZzetT Avatar asked Oct 16 '16 15:10

ZzetT


4 Answers

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';
like image 80
Justin Time - Reinstate Monica Avatar answered Nov 14 '22 00:11

Justin Time - Reinstate Monica


struct Builder
{
    enum { A = 1, B = 2, C = 4 };

    template<int f>
    using MyClass_t = MyClass<f & A, f & B, f & C>;

    template<int f>
    static MyClass_t<f> create()
    { return MyClass_t<f>(); }

};

Example of use (C++11):

auto my_obj = Builder::create<Builder::A | Builder::B>();

Or instead of a create method, if you want just faster access to the type:

Builder::MyClass_t<Builder::B> b;

If the opt_flag is put outside the class, and you don't need a create method, it's even shorter:

enum { optA = 1, optB = 2, optC = 4 };

template<int f>
using MyClass_t = MyClass<f & optA, f & optB, f & optC>;

int main()
{
    MyClass_t<optA | optB> my_obj;

    // ...
}

If only one of the template parameters is a typename, add it to the using declarator with the same default argument:

template<int f, class T = Class1B>
using MyClass_t = MyClass<f & optA, f & optB, f & optC, T>;

int main()
{
    MyClass_t<optA | optB, std::string> my_obj;
    MyClass_t<optB> my_obj_default_T;

    // ...
}
like image 32
Peregring-lk Avatar answered Nov 14 '22 00:11

Peregring-lk


You won't be able to do that as you are trying to create an instance of something at runtime that needs to be defined at compile time. Think of it this way, what would be the return type of create() ? It would need to be MyClass<> where you've specified the template parameters, but you are trying to do that at runtime, and they would actually need to be set at compile time.

Another option would be to make the options parameters to the constructor, rather than template parameters, and that way you can pass them in at construction.

class MyClass
{
public:
  MyClass(bool optionA, bool optionB, bool optionC);
};

class Builder
{
private:
  bool m_OptionA;
  bool m_OptionB;
  bool m_OptionC;

public:
  Builder()
  {
    m_OptionA = false;
    m_OptionB = false;
    m_OptionC = false;
  }

  Builder &useOptionA()
  {
    m_OptionA = true;
    return *this;
  }    

  Builder &useOptionB()
  {
    m_OptionB = true;
    return *this;
  }    

  Builder &useOptionC()
  {
    m_OptionC = true;
    return *this;
  }

  MyClass create() const
  {
    return MyClass(m_OptionA, m_OptionB, m_OptionC);
  }
};

Now you can say:

MyClass instance = Builder().useOptionA().useOptionB().create();
like image 2
Sean Avatar answered Nov 14 '22 01:11

Sean


Consider a mpl-like dictionary.

template<class T>struct tag_t{using type=T; constexpr tag_t(){};};
template<class T>constexpr tag_t<T> tag{};
template<class Tag>using type=typename Tag::type;

template<class K, class V, class...Base>
struct  map_entry:Base... {
  friend constexpr tag_t<V> get( tag_t<K>, map_entry ){ return {}; }
};

struct no_base{};
template<class Base, class...entries>
struct type_map;
template<class Base>
struct type_map<Base>:Base{};
template<class Base, class K0, class V0, class...B0s, class...Es>
struct type_map<Base, map_entry<K0,V0,B0s...>, Es...>:
  map_entry<K0,V0, type_map<Base, B0s..., Es...>>
{};

template<class Map, class Key>
using lookup=type<decltype(get(tag<Key>,std::declval<Map>())>;

To pass integral constants like bool, use:

template<bool b>using bool_k=std::integral_constant<bool, b>;

integral_constants.

We define tags:

namespace MyTags{
  struct OptionA{};
  struct OptionB{};
  struct OptionC{};
  struct T{};
}

populate defaults:

using MyClassDefaults=type_map<
  no_base,
  map_entry< MyTags::OptionA, bool_k<false> >,
  map_entry< MyTags::OptionB, bool_k<false> >,
  map_entry< MyTags::OptionC, bool_k<false> >,
  map_entry< MyTags::T, Class1B >
>;

template<class Options=MyClassDefaults>
class MyClass {
};

To read the values, do lookup<Options,MyTags::OptionA>. Create a using alias within the class to remove the need to repeat Options.

To override, simply:

using Override=type_map<
  MyClassDefaults,
  map_entry< MyTags::OptionC, bool_k<true> >
>;

Code not tested.

like image 2
Yakk - Adam Nevraumont Avatar answered Nov 14 '22 00:11

Yakk - Adam Nevraumont