Home > Blockchain >  Factory for a template class with enum template parameter
Factory for a template class with enum template parameter

Time:10-30

Suppose I have

enum class Colour
{
  red,
  blue,
  orange
};

class PencilBase
{
public:
  virtual void paint() = 0;
};

template <Colour c>
class Pencil : public PencilBase
{
  void paint() override
  {
    // use c
  }
};

Now I want to have some factory function to create painters

PencilBase* createColourPencil(Colour c);

What will be the most elegant way to implement this function?

I want to avoid making changes in this function (or its helpers) when I decide to introduce a new colour. I feel like we have all the information at compile time to achieve this, however I am having trouble to find a solution.

CodePudding user response:

Firstly, you need to know how many colors are there:

enum class Colour
{
    red,
    blue,
    orange,
    _count, // <--
};

After you know the number, you can create an array of function pointers of this size, each function creating the respective class. Then you use the enum as an index into the array, and call the function.

std::unique_ptr<PencilBase> createColourPencil(Colour c)
{
    if (c < Colour{} || c >= Colour::_count)
        throw std::runtime_error("Invalid color enum.");

    static constexpr auto funcs = []<std::size_t ...I>(std::index_sequence<I...>)
    {
        return std::array{ []() -> std::unique_ptr<PencilBase>
        {
            return std::make_unique<Pencil<Colour(I)>>();
        }...};
    }(std::make_index_sequence<std::size_t(Colour::_count)>{});

    return funcs[std::size_t(c)]();
}

Template lambas require C 20. If you replace the outer lambda with a function, it should work in C 17 as well.

MSVC doesn't like the inner lambda, so if you're using it, you might need to convert it to a function too. (GCC and Clang have no problem with it.)

I've used unique_ptr here, but nothing stops you from using the raw pointers.

CodePudding user response:

I don't think there are millions of solutions no ?

PencilBase* createColourPencil(Colour c)
{
    switch (c)
    {
        case Colour::red:
            return new Pencil<Colour::red> ();
        break;
        ...
        
        default:
        break;
    }
}

CodePudding user response:

You have several good possibilities:

PencilBase* createPencil (Colour colour) {
  switch (colour) {
    case RED:
      return new Pencil<RED>();
    case BLUE:
      return new Pencil<BLUE>();
...
    default:
      throw std::invalid_argument("Unsupported colour");
    }
}

or why not a more modern construct like this one, which has the advantage to be potentially updated at runtime:

std::unordered_map<Colour, std::function<PencilBase*()>> FACTORIES = {
  { RED, [](){ new Pencil<RED>(); } },
  { BLUE, [](){ new Pencil<BLUE>(); } },
...
};

PencilBase* createPencil (Colour colour) {
  return FACTORIES[colour]();
}

However, none can completely fullfill what you ask:

I want to avoid making changes in this function (or its helpers) when I decide to introduce a new colour. I feel like we have all the information at compile time to achieve this

There are at least two reasons for this:

  • Your colour parameter given to your factory is known at runtime, while your template parameter must be known at compile time.
  • There is no built-in solution to enumerate or go through all enum values

There isn't many solutions to go against the first point, except avoiding templates if you can. In your case, is there a good reason to use templates ? Do you really need to use a completely different class ? Can't you change your template into a normal parameter passed in the constructor or as a class member ?

If you have no choice to use templates, there exist a way to avoid repeating yourself. You may use good old macros and in a kind of special way commonly called X-macro. Quick example:

colours.hpp:

COLOUR(RED)
COLOUR(BLUE)
COLOUR(ORANGE)

Colour.hpp:

enum Colour {
#define COLOUR(C) C, 
#include "colours.hpp"
#undef COLOUR
INVALID_COLOUR
};

Factory function:

PencilBase* createPencil (Colour colour) {
  switch(colour) {
#define COLOUR(C) case C: return new Pencil<C>();
#include "colours.hpp"
#undef COLOUR
  default:
    throw new std::invalid_argument("Invalid colour");
  }
}

This will basicly rewrite the switch given as first example. You may as well change the macro to rewrite the mapping. However, as you can see, it may not be more readable or maintainable than to be explicit.

  • Related