Last Update:
Nice C++ Factory Implementation 2
Table of Contents
The original code from my previous post about “nice
factory”
did not work properly and I though there is no chance to fix it.
It appears, I was totally wrong! I got a really valuable feedback (even
with source code) and now I can present this improved version.
All credits should go to Matthew Vogt, who send me his version of the code and discussed the proposed solution.
The problem
Let me quickly recall the original problem:
There is a flawed factory method:
template <typename... Ts>
static std::unique_ptr<IRenderer>
create(const char *name, Ts&&... params)
{
std::string n{name};
if (n == "gl")
return std::unique_ptr<IRenderer>(
new GLRenderer(std::forward<Ts>(params)...));
else if (n == "dx")
return std::unique_ptr<IRenderer>(
new DXRenderer(std::forward<Ts>(params)...));
return nullptr;
}
I wanted to have one method that will create a desired object and that supports variable number of arguments (to match with constructors). This was based on the idea from the item 18 from Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14< />. Theoretically you could call:
auto pGL = create("gl", 10, "C:\data");
auto pDX = create("dx, "C:\shaders", 1024, 1024);
One method that is sort of a super factory.
Unfortunately, assuming each renderer has a different constructor
parameter list, the code above will not compile… the compiler cannot
compile just the part of this function (for one type) and skip the rest
(there is no static_if
).
So how to fix it?
Basic Idea
We need to provide function overloads that will return a proper type for
one set of parameters and nullptr
for everything else. So, we need to
enter a world of templates and that means compile time only! Let’s have
a look at the following approach:
template <typename... Ts>
unique_ptr<IRenderer>
create(const string &name, Ts&&... params)
{
if (name == "GL")
return construct<GLRenderer, Ts...>(forward<Ts>(params)...);
else if (name == "DX")
return construct<DXRenderer, Ts...>(forward<Ts>(params)...);
return nullptr;
}
We have a similar if
construction, but now we forward parameters to
the construct
function. This the crucial part of the whole solution.
The first function template overload (when we cannot match with the argument list) is quite obvious:
template <typename Concrete, typename... Ts>
unique_ptr<Concrete> construct(...)
{
return nullptr;
}
The second:
template <typename Concrete, typename... Ts>
std::enable_if_t<has_constructor, std::unique_ptr<Concrete> >
constructArgs(Ts&&... params)
{
return std::make_unique<Concrete>(std::forward<Ts>(params)...);
}
(has_constructor
is not a proper expression, will be defined later)
The idea here is quite simple: if our Concrete type has given
constructor (matching the parameter list) then we can use this version
of the function. Otherwise we fail and just return nullptr
. So we have
a classic example of SFINAE.
Let’s now look at the details… how to implement has_constructor
?
The details
Full code:
Online Compiler
example
The real function definition looks like that:
template <typename Concrete, typename... Ts>
enable_if_t<decltype(test_has_ctor<Concrete, Ts...>(nullptr))::value, unique_ptr<Concrete> >
constructArgs(Ts&&... params)
{
return std::make_unique<Concrete>(std::forward<Ts>(params)...);
}
test_has_ctor
tests if the Concrete type has the matching parameters:
template <typename U>
std::true_type test(U);
std::false_type test(...);
template <typename T, typename... Ts>
std::false_type test_has_ctor(...);
template <typename T, typename... Ts>
auto test_has_ctor(T*) -> decltype(test(declval< decltype(T(declval<Ts>()...)) >()));
Looks funny… right? :)
The core part is the matching:
decltype(test(declval<decltype(T(declval<Ts>()...)) >()))
In this expression we try to build a real object using given set of parameters. We simply try to call its constructor. Let’s read this part by part:
The most outer decltype
returns the type of the test
function
invocation. This might be true_type
or false_type
depending on what
version will be chosen.
Inside we have:
declval<decltype(T(declval<Ts>()...)) >()
Now, the most inner part ‘just’ calls the proper constructor. Then we
take a type out of that (should be T
) and create another value
that
can be passed to the test
function.
SFINAE in SFINAE… It’s probably better to look at some examples and what functions will be chosen.
If a type is invalid the SFINAE will occur in this constructor calling
expression. The whole function will be rejected from the overload
resolution set and we’ll just end up with test_has_ctor(...)
that
returns false_type
.
If a type has the right constructor, the matching expression will
properly build a object and it can be passed to test(U)
function. And
that will generate true_type
in the end.
Full code:
Online Compiler
example
Note: since C++14 you can use enable_if_t
(with the _t
suffix).
This is a template alias that greatly reduces length on expressions.
Look also for other similar aliases: with _t
or _v
suffixes in C++
type traits.
Final Thoughts
Although our solution works it’s still not that useful :) A valuable addition to that would be to parse an input string (or a script), generate types and values and then call a proper function. Like:
string s = "GL renderer tex.bmp 10 particles"
auto rend = create(s);
But that’s a whole other story.
Still, writing and understanding the described code was an great
experiment. To be honest, I needed to write those two posts before:
about SFINAE
and follow up to
get it right.
Once again many thanks goes to Matthew Vogt
I've prepared a valuable bonus for you!
Learn all major features of recent C++ Standards on my Reference Cards!
Check it out here:
Similar Articles:
- SFINAE Followup
- Micro benchmarking libraries for C++
- Simple Performance Timer
- C++ Status at the end of 2015
- C++ Status at the end of 2014