1

Can anybody suggest a technique for avoiding virtual template functions in the following code? I've read through several other posts, and I don't see how to apply those solutions to this case.

I'm building a library that contains a hierarchy of templated classes. I want to create an array of "factory" functions that can be used to instantiate derived classes by name (e.g., based on command line parameters).

If at all possible, I want each derived class to be able to register itself in it's own .hpp or .cpp file (as opposed to having to maintain a single list of all possible derived classes somewhere).

The code below almost works, except for the fatal flaw of trying to use a virtual template function.

//
// This code would appear in a library
//

template<class T>
class Base {
public:
  Base(const char* /*param*/) {}
};

//
// Description of each derived class.
// We need this non-templated base class so we can store all our
// descriptions in a single vector
//
class DescriptionBase {

private:
  const char *description;
  const char *name;

public:
  DescriptionBase(const char* pDesc, const char* pName) : description(pDesc), name(pName){
    // Whenever a Description object is created, it is automatically registered with the
    // global descriptionList.  This allows us to register derived classes in their own
    // .cpp/.hpp file (as opposed to keeping a centralized list of all derived classes).
    descriptionList.push_back(this);
  }

  // FAIL  Can't have virtual template functions
  virtual template<class T>
  Base<T> *make(const char *param) {return new Base<T>(param); }

  static vector<DescriptionBase *> descriptionList;
};

//global list of all derived classes
vector<DescriptionBase *> DescriptionBase::descriptionList;

// We use the template to store the type of the derived class 
// for use in the make method
template<template<typename> class D>
class Description : public DescriptionBase {

public:
  Description(const char* pDesc, const char* pName) : DescriptionBase(pDesc, pName) {}

  template<class T>
  Base<T> *make(const char *params) {
    return new D<T>(params);
  }
};

//
// These derived classes may be part of the library, or they may be
// written by users of the library.
//


template<class T>
class DerivedA : public Base<T> {
public:
  DerivedA(const char* param) : Base<T>(param) {return;}
};

Description<DerivedA> derivedA("derivedA", "This is the first derived class");

template<class T>
class DerivedB : public Base<T> {
  DerivedB(const char* param) : Base<T>(param) {return;}
};
Description<DerivedA> derivedB("derivedA", "This is the second derived class");

//
// Example code written by the user of the library.
//
//


int main(int argc, char *argv[]) {

  // Using a descriptionList is just a short-cut here.
  // Final code will use a map.
  int indexOfDerivedA = 0; 

  Base<int> *intItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<int>("parameter to derived type's constructor");
  Base<char> *charItem = DescriptionBase::descriptionList[indexOfDerivedA]->make<char>("parameter to derived type's constructor");


}
Zack
  • 6,232
  • 8
  • 38
  • 68
  • 1
    If you don't know the type of the return value, then you can't use the constructor sensibly, so they either need to have a common return type (so you can actually assign it to something) or they should be separately named factories. The type system can't know what type a "fred" is or a "george", but it has to determine at compile time if the call is valid. It's important to remember that Base and Base aren't related in any way from the type system's perspective. – xaxxon Jan 12 '17 at 19:19
  • I didnt read your code but you can do that by having a map of strings to functors that return an object matching an interface. Classes register themselves by inserting a functor in that map. – imreal Jan 12 '17 at 19:22
  • @xaxxon I modified the code to be more clear. "fred" and "george" were parameters to the constructor, not the type names. Base and Base aren't intended to be related. I got stuck at the point of building the `descriptionList` in the library (which, of course, doesn't know what template parameters the users will select). – Zack Jan 12 '17 at 19:41
  • @imreal The problem with using a functor is that the objects returned by the functors are templated. The library can define the functors, but it can't instantiate them because it doesn't know that types the users will select as template parameters. – Zack Jan 12 '17 at 19:42
  • Is there a finite bounded list of `T` that can be passed to `Base`? Can you at least vaguely describe your actual practical problem you are solving, to see if you are just running up a dead-end? You want by-name lookup of a template of factories? What kind of linking is required? How much code can I request be put in common .h files? How, exactly, do `Base` differ from each other? – Yakk - Adam Nevraumont Jan 12 '17 at 19:44
  • Arent they all in the same interface hierarchy? – imreal Jan 12 '17 at 19:46
  • @Yakk Perhaps. The actual implementation is code used to analyze graphs. So, the template types are just various widths of integers (uint8_t, uint32_t, unit64_t, etc.) The target audience is undergraduates, so other goals include making the addition of new derived classes as simple as possible, and avoiding complicated build dependencies like boost. – Zack Jan 12 '17 at 19:47
  • @Zack Well, if you are restricting `T` to integers, this problem is easy. – Yakk - Adam Nevraumont Jan 12 '17 at 19:48
  • @imreal If I were to use a function pointer as a factory, it would be defined link this: ` typedef Base* (*Factory)(const char* param)` I'm trying to figure out how to get `DerivedB` to add itself to an array of these pointers, without maintaining an explicit list of derived classes. In other words, I'd like to be able to call a method like 'generateFactoryList()' and have that method automatically "find" all derived classes and create the factory function. I can't figure out how to make all derived classes known to this function without enumerating them. – Zack Jan 12 '17 at 19:54
  • @Yakk Let me add another wrinkle: The actual class uses three template parameters, not one. So, the full list of all possibilities would be 64, not 4. – Zack Jan 12 '17 at 19:56

1 Answers1

2

This is a compile-time tag and type:

template<class T>
struct tag_t { constexpr tag_t() {}; };
template<class T> constexpr tag_t tag<T>{};

This is a list of types:

template<class...>struct types_t{constexpr types_t(){}; using type=types_t;};
template<class...Ts>constexpr types_t<Ts...> types{};

This maps the content of a list of types:

template<template<class...>class Z, class types>
struct fmap{};
template<template<class...>class Z, class types>
using fmap_t=typename fmap<Z,types>::type;
template<template<class...>class Z, template<class...>class types, class...Ts>
struct fmap<Z,types<Ts...>> {
  using type=types<Z<Ts...>>;
};

Now lets make a list of factories:

template<class...Args>
struct build_tagged_sig {
  template<class T>
  using result = std::unique_ptr<T>(tag_t<T>,Args...);
};
template<template<class...>class Out, class types, class...Args>
using tagged_factories =
  fmap_t<
    std::function,
    fmap_t<
      build_tagged_sig<Args...>::template result,
      fmap_t< Out, types >
    >
  >;

This applies a types to a template:

template<template<class...>class Z, class types>
struct apply_types {};
template<template<class...>class Z, class types>
using apply_types_t = typename apply_types<Z,types>::type;
template<template<class...>class Z, template<class...>class types, class...Ts>
struct apply_types<Z, types<Ts...>> {
  using type=Z<Ts...>;
};
template<template<class...>class Z>
struct applier {
  template<class types>
  using result=apply_types_t<Z,types>;
};

This SO post shows how to overload multiple lambdas or std::functions.

using my_types = types_t< std::int8_t, std::int16_t, std::int32_t, std::int64_t >;

using magic_factory =
  apply_types_t<
    overload,
    tagged_factories< Base, my_types, const char* > >
  >;

which is an overload over

std::function< std::unique_ptr<Base<std::int8_t>>( tag_t<Base<std::int8_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int16_t>>( tag_t<Base<std::int16_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int32_t>>( tag_t<Base<std::int32_t>>, const char* ) >,
std::function< std::unique_ptr<Base<std::int64_t>>( tag_t<Base<std::int64_t>>, const char* ) >

We now write a register factory:

template<class F, class...Ts>
magic_factory make_poly_factory( types_t<Ts...>, F&& f ) {
  return magic_factory(
    (void(tag<Ts>), f)...
  );
}
template<class F>
magic_factory make_poly_factory( F&& f ) {
  return make_poly_factory( my_types{}, f );
}

which creates N copies of f and stores each in a std::function all in one object.

Taking the return value, you can call an individual one by overload resolution.

template<class T>
std::unique_ptr<Base<T>> factory_A_impl( tag_t<Base<T>>, const char* param) {
  return new DerivedA<T>(param);
}
auto magic = magic_factory( my_types{}, [](auto tag, const char* param){
  return factory_A_impl( tag, param );
});

std::unique_ptr<Base<std::int8_t>> bob = magic( tag<std::int8_t>, "hello" );

and bob is a unique_ptr to a Base<std::int8_t> that is actually a DerivedA<std::int8_t> at runtime.

This probably has tpyos.

Most of this post was the metaprogramming to set up the single object that overloads each of tag_t<T0> through tag_t<T1> without repeating myself. You can do that manually for your 4 types.

The overload I used presumes we aren't taking a single lambda with template arguments and type erasing each overload, but instead a set of lambdas. The first would make a few things above a touch easier.

The end user student just has to create a function object that takes a tag_t<X> and a const char* and returns a unique_ptr<X> and is cheap-to-copy, then create a magic_factory from it (which type-erases it down into a bundle of std::functions).

DescriptionBase becomes:

struct Description {
  char const* description = 0;
  char const* name = 0;
  magic_factory factory;
  template<class T>
  std::unique_ptr<Base<T>>
  make(const char *param) {
    return factory( tag<T>, param );
  }
};

The polymorphism is now within magic_factory. Store instances of Description, not pointers to them, as they are value-type polymorphism.

See, easy.

Adding more template parameters just adds some more complexity to the fmap stuff, and more responsibilities to the creator of the magic_factory.

You'll want a cross-product operation to generate the 64 different sets of types from the one list of 4 elements. It will be a types_t of types_t.

Call this my_many_types.

Then

using magic_factory =
  apply_types_t<
    overload,
    tagged_factories< applier<Base>::template result, my_types, const char* > >
  >;

and done, we now have 64 overloads with signatures like:

std::unique_ptr<Base<std::int8_t, std::int16_t, std::int8_t>>(
  tag_t<Base<std::int8_t, std::int16_t, std::int8_t>>,
  const char*
)

Now we could just do all of this manually.

Build a table like this:

template<class T>
using factory_ptr = std::unique_ptr<T>( void*, tag_t<T>, const char* );

using factory_table = std::tuple<
  factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int16_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int32_t > >,
  factory_ptr< Base< std::int8_t, std::int8_t, std::int64_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int8_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int16_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int32_t > >,
  factory_ptr< Base< std::int8_t, std::int16_t, std::int64_t > >,

...

  factory_ptr< Base< std::int64_t, std::int64_t, std::int64_t > >
>;

Now a magic factory:

struct magic_factory {
  std::unique_ptr<void, void(*)(void*)> state;
  factory_table table;

  template<class T0, class T1, class T2>
  std::unique_ptr<Base<T0, T1, T2>> make( char const* param ) {
    auto f = std::get<factory_ptr< Base< T0, T1, T2 > >>( table );
    return f( state.get(), param );
  }

  magic_factory(magic_factory&&)=default;
  template<class T,
    class=std::enable_if_t<!std::is_same<std::decay_t<T>, magic_factory>::value>
  >
  magic_factory( T&& t ) {
    ptr = {new std::decay_t<T> >(std::forward<T>(t)),
      [](void* ptr){
        delete static_cast< std::decay_t<T>* >(ptr);
      }
    };
    // 64 lines like this:
    std::get<factory_ptr< Base< std::int8_t, std::int8_t, std::int8_t > >>( table )
    = +[](void* pvoid, tag_t<Base< std::int8_t, std::int8_t, std::int8_t >> tag, char const* param)->std::unique_ptr<Base< std::int8_t, std::int8_t, std::int8_t >>
    {
      auto*pt = static_cast<std::decay_t<T>*>(pvoid);
      return (pt)(tag, param);
    };
  }
};

where we build a tuple of type-erasure functions and a void pointer to its argument and dispatch ourselves.

You can also use some of the above machinery to automate this. The manual table does give efficiency, as we don't duplicate the state of the callable object N times like with the std::function version.


Another approach is to use my type erasing type erasure solution.

We write a set of template any_methods that do the tag trick to create the Base<A,B,C> object.

We then create a super_any on each of the any_methods.

Then we inherit from it and wrap your make to dispatch to those any_methods.

This could be about as efficient as the manual approach just above.

template<class T0, class T1, class T2>
auto make_base = make_any_method<std::unique_ptr<Base<T0, T1, T2>>(char const* name)>(
  [](auto* p, char const* name)
  {
    return p->make<T0, T1, T2>( name );
  }
);
template<class T0, class T1, class T2>
using base_maker = decltype(make_base);

Now our factory is a:

super_any< base_maker<std::int8_t, std::int8_t, std::int8_t > > bob;

bob can store a pointer to a class that implements make<T0, T1, T2> where they match int8_t, int8_t, int8_t.

Add 64 more lines and done.

The types that bob stores need not have a common base class at all. It doesn't use C++ inheritance to implement polymorphism, but instead manual type erasure.

To call it you just

auto r = (bob->*make_base<std::int8_t, std::int8_t, std::int8_t>)("hello");

Naturally pass more types to super_any and it suppports more types of make_base.

Community
  • 1
  • 1
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524