I have an issue I'm facing where I'm trying to build a factory function that, given an ID and a type will return the correct (templated) subclass.
What this is trying to solve:
The id()
values are sent across a network as soon as a connection is established, and specify to the receiver how a sequence of bytes are encoded. The receiver knows in advance the type T that it expects, but does not know how that type T is encoded on the wire until it gets this value. It also specifies how return values (of some type U, where U may or may not be the same type as T) should be marshalled when they are returned. This code is used generally, i.e. there are multiple senders/receivers that use/expect different types; the types used between a given sender/receiver pair are always fixed, however.
A basic sketch of the problem: we have a (simplified) base class that defines id()
template <typename T>
class foo
{
public:
virtual ~foo() { }
// Other methods
// This must return the same value for every type T
virtual std::uint8_t id() const noexcept = 0;
};
From there, we have some subclasses:
template <typename T>
class bar : public foo<T>
{
public:
std::uint8_t id() const noexcept override { return 1; }
};
template <typename T>
class quux : public foo<T>
{
public:
std::uint8_t id() const noexcept override { return 2; }
};
For the actual factory function, I need to store something that erases the type (e.g. bar, quux) so that I can store the actual creation function in a homogenous container. Effectively, I want semantics that are roughly equivalent to:
struct creation_holder
{
// Obviously this cannot work, as we cannot have virtual template functions
template <typename T>
virtual foo<T>* build() const;
};
template <template <typename> class F>
struct create : public creation_holder
{
// As above
template <typename T>
foo<T>* build() const override
{
return new F<T>();
}
};
std::unordered_map<std::uint8_t, create*>& mapping()
{
static std::unordered_map<std::uint8_t, create*> m;
return m;
}
template <typename T, template <typename> class F>
bool register_foo(F<T> foo_subclass,
typename std::enable_if<std::is_base_of<foo<T>, F<T>>::value>::type* = 0)
{
auto& m = mapping();
const auto id = foo_subclass.id();
creation_holder* hold = new create<F>();
// insert into map if it's not already present
}
template <typename T>
foo<T>* from_id(std::uint8_t id)
{
const auto& m = mapping();
auto it = m.find(id);
if(it == m.end()) { return nullptr; }
auto c = it->second;
return c->build<T>();
}
I've played around with a number of ideas to try and get something with similar semantics, but with no luck. Is there a way to do this (I don't care if the implementation is significantly different).
Some utility types for passing around types and bundles of types:
Now we write a poly ifactory.
Here is an
ifactory
:you pass in the tag you want to build and you get out an object. Pretty simple.
We then bundle them up (this would be easier in c++171, but you asked for c++11):
The one type case:
the 2+ case:
We import the
tagged_build
method down into the derived classes. This means that the most-derivedpoly_ifactory_impl
has all of thetagged_build
methods in the same overload set. We'll use this to dispatch to them.Then we wrap it up pretty:
notice I'm returning a
unique_ptr
; returing a rawT*
from a factory method is code smell.Someone with a
poly_ifactory<?>
just does a->build<T>()
and ignores thetagged_
overloads (unless they want them; I leave them exposed). Eachtagged_build
is virtual, butbuild<T>
is not. This is how we emulate a virtual template function.This handles the interface. At the other end we don't want to have to implement each
build(tag_t<T>)
manually. We can solve this with the CRTP.the 1 type case:
the 2+ type case:
what this does is write a series of
tagged_build(tag_t<T>)
overloads of theifactory
methods, and redirects them toD::build_impl(tag_t<T>)
, whereD
is a theoretical derived type.The fancy "pass Base around" exists to avoid having to use virtual inheritance. We inherit linearly, each step implementing one
tagged_build(tag<T>)
overload. All of them dispatch downward non-virtually using CRTP.Use looks like:
and an instance of
my_factory
satisfies themy_ifactory
interface.In this case, we create a unique ptr to a vector of
T
with a number of elements equal tosizeof(T)
. It is just a toy.Live example.
The pseudo code design.
The interface has a
function. It dispatches to
methods.
The
T
s in question are extracted from atypes_t<Ts...>
list. Only those types are supported.On the implementation side, we create a linear inheritance of CRTP helpers. Each inherits from the last, and overrides a
virtual R tagged_build(tag_t<T>)
.The implementation of
tagged_build
uses CRTP to cast thethis
pointer to a more-derived class and callbuild_impl(tag<T>)
on it. This is an example of non-runtime polymorphism.So calls go
build<T>
tovirtual tagged_build(tag_t<T>)
tobuild_impl(tag<T>)
. Users just interact with one template; implementors just implement one template. The glue in the middle -- thevirtual tagged_build
-- is generated from atypes_t
list of types.This is about 100 lines of "glue" or helper code, and in exchange we get effectively virtual template methods.
1 in c++17 this becomes:
which is much simpler and clearer.
Finally, you can do something vaguely like this without a central list of types. If you know both the caller and the callee know the type you could pass a
typeid
ortypeindex
into theifactory
, pass avoid*
or something similar out over the virtual dispatch mechanism, and cast/check for null/do a lookup in a map to types.The internal implementation would look similar to this one, but you wouldn't have to publish
types_t
as part of your formal (or binary) interface.Externally, you would have to "just know" what types are supported. At runtime, you might get a null smart (or dumb, ick) pointer out if you pass in an unsupported type.
With a bit of care you could even do both. Expose an efficient, safe mechanism to get compile-time known types applied to a template. Also expose a "try" based interface that both uses the efficient compile-time known system (if the type matches) and falls back on the inefficient runtime checked on. You might do this for esoteric backwards binary compatibility reasons (so new software can connect over an obsolete interface to new or old API implementations and handle having an old API implementation dynamically).
But at that point, have you considered using COM?