Partners: KDAB Whole Tomato Software CppDepend

03 March 2016

Nice C++ Factory Implementation 2

Nice C++ Factory Implementation

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

Get my free ebook about C++17!

More than 50 pages about the new Language Standard.

C++17 in detail, by Bartlomiej Filipek

© 2017, Bartlomiej Filipek, Blogger platform
Any opinions expressed herein are in no way representative of those of my employers.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.