C++ Factory Pattern with Heterogenous Constructor Constraint
Asked Answered
H

3

6

I'm implementing a C++ program that can programmatically instantiate objects given an input file which provides the class names and arguments to pass to the constructors.

The classes are derived from a common base class but their constructor signature varies.

They are declared as follows:

class Base { ... }
class Class1 : Base { Class1(int a1, int a2); }
class Class2 : Base { Class2(int a1, int a2, int a3); }
... and so on...

The argument types do not have to be int's, in fact they could be any built-in type or complex, custom type.

The program input could look like this in JSON form:

[
  { "Class1": ["arg11", "arg12"] },
  { "Class2": ["arg21", "arg22", "arg23"] },
  ...and so on...
]

Reading through the docs for Boost.Functional/Factory it appears that it could solve my problem were it not for the fact that in my application the constructor signature varies (the heterogeneity constraint). Boost.Function/Factory's approach is to normalize the constructor signatures however this is not possible in my application.

In a dynamic language like Python, this would be rather trivial: obj = klass(*args) where klass = Class1 and args = ["arg11, "arg12"].

So how would one go about implementing the factory pattern with the heterogenous constraint in C++?

Are there other libraries besides Boost that I have overlooked that may be of assistance?

Is it possible to implement this such that the only dependency is the standard library (i.e. no Boost)?

Furthermore, in the case where a constructor argument is of a complex type so that it must be specially constructed from its JSON representation, how does it affect the complexity of the problem?

Hulahula answered 19/12, 2011 at 0:27 Comment(8)
For future reference, the opposite of homogeneous is heterogeneousCorticosterone
Hey Seth, 'inhomogeneous' is also a valid word according to the Merriam-Webster dictionary and is similar in meaning to 'heterogeneous'. I chose the former because that is also the choice of Boost.Function/Factory documentation (see the link in my post).Hulahula
Yes I know, you can add "in" or "un" to almost any word and it will still be a word. It just sounds weird.Corticosterone
@Salman: The more references I see for Merriam-Webster, the more I think it's a massive pile of junk. Next time, try the Oxford English Dictionary.Sneaky
@DeadMG I don't care what you think about the choice of dictionaries. It's irrelevant to the original question. Either choice of word would have sufficed. However, if for stylistic reasons one is preferred over the other, that's fine with me. But please don't turn this into a discussion about language dictionaries which will quickly degenerate into a discussion about search engine results, etc. Please stick to the question. Thanks!Hulahula
well maybe JSON was not the best choice for serialization of C++ classes... why not use boost.serialization instead?Transnational
@Transnational Good point. However in this application, the program input has to be a file in user editable format. JSON and YAML seem to fit the bill well for that purpose. I'm guessing that the boost.serialization file format is binary?Hulahula
@SalmanHaq: AFAIK boost serialization supports XML, but those xml files are not designed to be read or edited by users.Transnational
R
1

To achieve what you want you will need, at some point in your code, a giant switch-statement to decide which class to construct based on the name (actually, a switch won't work, because you can't switch on strings - more like a very long if-else if).

Also, it seems that the representation you show does not contain any information about the type of the constructor arguments. This could be a problem if you have a class that has multiple constructors callable with the same number of arguments.

In the end, I think it is best if you go with something like @selbies answer, but use code-generation to generate the construction-code for you.

Regin answered 20/12, 2011 at 7:31 Comment(1)
Thanks @bjorn & selbie. For my use case, the best answer is code generation. My code generator is ~100 lines of Python and generates approximately valid factory functions. I.e. I have to massage some of the generated code. Even with a less than perfect code generator, I have been incredibly productive in accomplishing this task. It might still be worth extending Boost.Functional/Factory for my use case. To be explored later. :) FYI: C++ header parsing library: CppHeaderParser library, which is adequate for my needs.Hulahula
T
5

Have you considered having a factory method for each class that knows how to construct the object from an "array" of parameters read from the file.

That is:

// declared "static" in header file
Class1* Class1::FactoryCreate(int argc, const char** argv)
{
    if (argc != 2)
        return NULL; // error

    int a1 = atoi(argv[0]);
    int a2 = atoi(argv[1]);
    return new Class1(a1, a2, a3);
}

// declared "static" in header file
Class2* Class2::FactoryCreate(int argc, const char** argv)
{
    if (argc != 3)
        return NULL; // error
    int a1 = atoi(argv[0]);
    int a2 = atoi(argv[1]);
    int a3 = atoi(argv[2]);
    return new Class2(a1, a2, a3);
}
Trinitrocresol answered 19/12, 2011 at 21:35 Comment(1)
I have certainly considered an option similar to this... i.e. to pass the JSON object to a static factory function and have it return the instance. The issue is that the code base I'm dealing with has 100+ such classes at this point and growing. So while this would certainly be a possible solution, I hesitate to call it ideal at the moment.Hulahula
R
1

To achieve what you want you will need, at some point in your code, a giant switch-statement to decide which class to construct based on the name (actually, a switch won't work, because you can't switch on strings - more like a very long if-else if).

Also, it seems that the representation you show does not contain any information about the type of the constructor arguments. This could be a problem if you have a class that has multiple constructors callable with the same number of arguments.

In the end, I think it is best if you go with something like @selbies answer, but use code-generation to generate the construction-code for you.

Regin answered 20/12, 2011 at 7:31 Comment(1)
Thanks @bjorn & selbie. For my use case, the best answer is code generation. My code generator is ~100 lines of Python and generates approximately valid factory functions. I.e. I have to massage some of the generated code. Even with a less than perfect code generator, I have been incredibly productive in accomplishing this task. It might still be worth extending Boost.Functional/Factory for my use case. To be explored later. :) FYI: C++ header parsing library: CppHeaderParser library, which is adequate for my needs.Hulahula
S
0

I know I am a bit late, but there is a good modern solution for C++17 heterogeneous_factory. The main idea is to use type erasure with std::any that allows you to store different constructor trait types. The library is a bit specific, but fully documented and tested.

Here is the minimal example to get a basic idea of described method:

template <class BaseT>
class Factory
{
    using BasePtrT = std::unique_ptr<BaseT>;
public:
    template<class RegistredT, typename... Args>
    void registerType(const std::string& name)
    {
        using CreatorTraitT = std::function<BasePtrT(Args...)>;
        CreatorTraitT trait = [](Args... args) {
            return std::make_unique<RegistredT>(args...);
        };
        _traits.emplace(name, trait);
    }

    template<typename... Args>
    BasePtrT create(const std::string& name, Args... args)
    {
        using CreatorTraitT = std::function<BasePtrT(Args...)>;
        const auto found_it = _traits.find(name);
        if (found_it == _traits.end()) {
            return nullptr;
        }
        try {
            auto creator = std::any_cast<CreatorTraitT>(found_it->second);
            return creator(std::forward<Args>(args)...);
        }
        catch (const std::bad_any_cast&) {}
        return nullptr;
    }

private:
    std::map<std::string, std::any> _traits;
};

struct Interface
{
    virtual ~Interface() = default;
};

struct Concrete : Interface
{
    Concrete(int a) {};
};

struct ConcreteSecond : Interface
{
    ConcreteSecond(int a, int b) {};
};

int main() {
    Factory<Interface> factory;
    factory.registerType<Concrete, int>("concrete");
    factory.registerType<ConcreteSecond, int, int>("second_concrete");
    assert(factory.create("concrete", 1) != nullptr);
    assert(factory.create("concrete") == nullptr);
    assert(factory.create("second_concrete", 1) == nullptr);
    assert(factory.create("second_concrete", 1, 2) != nullptr);
}
Satori answered 30/1, 2020 at 11:9 Comment(1)
From Review: Would you please post some code in your answer? Links may get broken over time.Cedilla

© 2022 - 2024 — McMap. All rights reserved.