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::function
s.
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::function
s).
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_method
s that do the tag trick to create the Base<A,B,C>
object.
We then create a super_any
on each of the any_method
s.
Then we inherit from it and wrap your make
to dispatch to those any_method
s.
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
.