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?

Salman Haq
  • 340
  • 3
  • 11
  • For future reference, the opposite of homogeneous is heterogeneous – Seth Carnegie Dec 19 '11 at 00:31
  • 2
    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). – Salman Haq Dec 19 '11 at 01:25
  • Yes I know, you can add "in" or "un" to almost any word and it will still be a word. It just sounds weird. – Seth Carnegie Dec 19 '11 at 01:26
  • @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. – Puppy Dec 19 '11 at 22:03
  • 2
    @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! – Salman Haq Dec 19 '11 at 22:56
  • well maybe JSON was not the best choice for serialization of C++ classes... why not use boost.serialization instead? – smerlin Dec 20 '11 at 07:33
  • @smerlin 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? – Salman Haq Dec 20 '11 at 20:49
  • @SalmanHaq: AFAIK boost serialization supports XML, but those xml files are not designed to be read or edited by users. – smerlin Dec 20 '11 at 22:34

3 Answers3

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);
}
selbie
  • 100,020
  • 15
  • 103
  • 173
  • 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. – Salman Haq Dec 19 '11 at 23:05
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.

Community
  • 1
  • 1
Björn Pollex
  • 75,346
  • 28
  • 201
  • 283
  • 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](https://bitbucket.org/senex/cppheaderparser/src), which is adequate for my needs. – Salman Haq Dec 29 '11 at 22:45
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);
}
farmov
  • 1
  • 1