Automatic compile-time factory registration of class templates in C++
Asked Answered
A

2

11

I'm looking for an abstract factory for class templates, where the classes register themselves automatically at static initialization time. For regular (non-templated) classes, the solution is straightforward enough using static members. Here's an example of a (rather simplistic) solution that works just fine:

#include <cassert>
#include <iostream>

class Base {
 public:
  virtual size_t id() const = 0;
  virtual const char* name() const = 0;
  virtual ~Base() {}
};

typedef Base* (*CreateFunc)(void);

class SimpleFactory {
 private:
  static const size_t NELEM = 2;
  static size_t id_;
  static CreateFunc creators_[NELEM];

 public:
  static size_t registerFunc(CreateFunc creator) {
    assert(id_ < NELEM);
    assert(creator);
    creators_[id_] = creator;
    return id_++;
  }

  static Base* create(size_t id) { assert(id < NELEM); return (creators_[id])(); }
};

size_t SimpleFactory::id_ = 0;
CreateFunc SimpleFactory::creators_[NELEM];


class D1 : public Base {
 private:
  static Base* create() { return new D1; }
  static const size_t id_;

 public:
  size_t id() const { return id_; }
  const char* name() const { return "D1"; }
};

const size_t D1::id_ = SimpleFactory::registerFunc(&create);

class D2 : public Base {
 private:
  static Base* create() { return new D2; }
  static const size_t id_;

 public:
  size_t id() const { return id_; }
  const char* name() const { return "D2"; }
};

const size_t D2::id_ = SimpleFactory::registerFunc(&create);

int main() {
  Base* b1 = SimpleFactory::create(0);
  Base* b2 = SimpleFactory::create(1);
  std::cout << "b1 name: " << b1->name() << "\tid: " << b1->id() << "\n";
  std::cout << "b2 name: " << b2->name() << "\tid: " << b2->id() << "\n";
  delete b1;
  delete b2;
  return 0;
}

The question I have is how to make it work when the stuff I want to register/create is more like:

template <typename T> class Base...
template <typename T> class D1 : public Base<T> ...

The best idea I can think of is to template the factory as well, something like:

 template <typename T>
 class SimpleFactory {
 private:
  static const size_t NELEM = 2;
  static size_t id_;
  typedef Base<T>* Creator;
  static Creator creators_[NELEM];
...(the rest remains largely the same)

But I'm wondering if there's a better way, or if someone has implemented such a pattern before.

EDIT: revisiting this problem a few years later (and with variadic templates), I can get much closer to what I want by simply "registering" functions, or rather classes, as template parameters to the factory. It would look something like this:

#include <cassert>

struct Base {};

struct A : public Base {
  A() { std::cout << "A" << std::endl; }
};

struct B : public Base {
  B() { std::cout << "B" << std::endl; }
};

struct C : public Base {
  C() { std::cout << "C" << std::endl; }
};

struct D : public Base {
  D() { std::cout << "D" << std::endl; }
};


namespace {
  template <class Head>
  std::unique_ptr<Base>
  createAux(unsigned id)
  {
    assert(id == 0);
    return std::make_unique<Head>();
  }

  template <class Head, class Second, class... Tail>
  std::unique_ptr<Base>
  createAux(unsigned id)
  {
    if (id == 0) {
      return std::make_unique<Head>();
    } else {
      return createAux<Second, Tail...>(id - 1);
    }
  }
}

template <class... Types>
class LetterFactory {
 public:
  std::unique_ptr<Base>
  create(unsigned id) const
  {
    static_assert(sizeof...(Types) > 0, "Need at least one type for factory");
    assert(id < sizeof...(Types));
    return createAux<Types...>(id);
  }
};

int main() {
  LetterFactory<A, B, C, D> fac;
  fac.create(3);
  return 0;
}

Now, this is just a simplistic prototype, so never mind create()'s linear complexity. The main deficiency of this design, however, is that it doesn't allow for any constructor parameters. Ideally, I'd be able to register not only the classes the factory needs to create, but also the types each class takes in its constructor, and let create() take them variadically. Has anyone ever done something like this before?

Astragal answered 6/10, 2011 at 19:15 Comment(3)
Contrary to popular belief, your functions do not register themselves at compile time, but rather at program start, before main() is called. This is in part because registerFunc is not declared as constexpr, but also because it isn't really possible the way you wrote it.Claus
If you want to get fancy, you could make a single, global creator registry of type std::map<std::type_info, void(*)()>. To retrieve templated by type T you'd cast m[typeid(T)] to Base<T>(*create)().Claus
Of course you're right about compile time/init time. Sorry for the confusion. As for your idea for type erasure--it's not bad. It would have been pretty though if I could have hidden those details in a definition file, but the T template prevents that.Astragal
H
3

I posted an answer to a similar issue over at GameDev, but the solution is not compile time. You can check it out here:
> https://gamedev.stackexchange.com/questions/17746/entity-component-systems-in-c-how-do-i-discover-types-and-construct-components/17759#17759

I don't think there's even a way to make this compile time. Your "id" inside of the base class is really just a simplified form of RTTI, which is by definition run-time. Maybe if you made the id a template argument... but that would make some other things a lot more complicated.

Hobbes answered 6/10, 2011 at 19:53 Comment(3)
You're right about compile time, of course. I edited the original question to clear out the confusion.Astragal
Correct me if I'm wrong, but your post is merely a cleaner way to implement original example I posted above. It does not seem to solve the template class factory problem.Astragal
Hmm, you're right. If you want to keep only one factory, then the only option is to have a non-template base class. Maybe it'd be a good idea to post a sketch of what an ideal interface would look like for you. :)Hobbes
F
-2

Doing simpler things will break less and makes your intentions obvious.

int main() {
  RegisterConcreteTypeFoo();
  RegisterConcreteTypeBar();
  // do stuff...
  CleanupFactories();
  return 0;
}

When those init functions are actually called (not at compile time) and they fail you won't get all the pretty easy things that makes debugging easier. Like a stack trace.

With this scenario you also assume you won't want to initialize them differently. It is overly complicated to unit test "automatically registered" anything, for instance.

Less magic = easier, cheaper maintenance.

If that weren't enough, there are technical problems too. Compilers like to strip out unused symbols from libraries. There may be a compiler specific shenanigan to get around it, I'm not sure. Hopefully it does it in a consistent way, instead of randomly for no obvious reason in the middle of a development cycle.

Freudberg answered 6/10, 2011 at 19:30 Comment(5)
I agree with the general principle that less magic is easier. I appreciate that you're trying to direct me to do the general "Right Thing". Please trust that I am well informed on the simple solution, and have intentionally and specifically asked for this corner case. My actual usage is more complex than the simplistic example I demonstrated, because I don't want to confuse the question with details that aren't relevant to the specific technical challenge.Astragal
The short story on why I'm insisting on automatic registration is scalability and extensibility: I plan to have many objects in the hierarchy, and I don't want to forget registering any of them manually. More over, the library is designed for anyone to add their own derived objects without having to touch a central point of registration. Arguing this further would require more context--but can we focus on my original question instead?Astragal
@Tom This would actually be pretty hard to maintain in larger projects. Every time you add or remove a class you also have to add one more function call in an entirely different file and maybe even create a different register function for each type.Hobbes
Your original question goes against industry best practice, regardless of whether you think your circumstances are special.Freudberg
@Paul It is not hard to maintain, it is tedious though. I've done this in large projects and it was absolutely fine. You guys make it sound like writing three lines of boiler plate a few thousand times will turn into a career.Freudberg

© 2022 - 2024 — McMap. All rights reserved.