7

Looking for a way to avoid a massive IF/ELSE and use a lookup table to resolve strings to particular classes to instantiate, that all derive from a base class. Is something like this possible, and if so, how?

typedef struct BaseClass
{
} BaseClass;

typedef struct DerivedClassOne : BaseClass
{
} DerivedClassOne;

typedef struct DerivedClassTwo : BaseClass
{
} DerivedClassTwo;

typedef struct
{
    const char *name;
    BaseClass class;
} LookupList;

LookupList list[] = {
    {"ClassOne", DerivedClassOne},
    {"ClassTwo", DerivedClassTwo}
};

BaseClass *InstantiateFromString(char *name)
{
    int i;
    for (i = 0; i < 2; i++)
    {
        if (!strcmp(name, list[i].name))
            return new list[i].class();
    }
}

int main (int argc, char *argv[])
{
    BaseClass *myObjectFromLookup = InstantiateFromString("ClassOne");
}
user1054922
  • 2,101
  • 2
  • 23
  • 37
  • You don't need `typedef class` in C++ just so you know. – David G Jul 20 '13 at 12:25
  • 5
    Why not use a `std::map` instead (where `factory` is the prototype of an appropriate factory function, of course). Point being, don’t use C-style strings and linear search, use the appropriate classes. – Konrad Rudolph Jul 20 '13 at 12:34
  • Check out the factory pattern. http://stackoverflow.com/questions/5120768/how-to-implement-the-factory-pattern-in-c-correctly – fishfood Jul 20 '13 at 13:12

4 Answers4

6

If your compiler is compatible with C++11, you can easily do that with lambdas and std::map:

#include <iostream>
#include <string>
#include <map>
#include <functional>

using namespace std;

struct BaseClass {virtual void foo()=0;};
struct DerivedClass1 : public BaseClass {void foo() {cout << "1" << endl;}};
struct DerivedClass2 : public BaseClass {void foo() {cout << "2" << endl;}};

// Here is the core of the solution: this map of lambdas does all the "magic"
map<string,function<BaseClass*()> > factory {
    {"one", [](){return new DerivedClass1();}}
,   {"two", [](){return new DerivedClass2();}}
};

int main() {
    BaseClass *a = factory["one"](); // Note the function call () at the end
    BaseClass *b = factory["two"]();
    a->foo();
    b->foo();
    delete a;
    delete b;
    return 0;
}

The idea is to make a map that gives you a function that makes an appropriate subclass.

Demo on ideone.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
  • 1
    Thanks, this is the correct answer, although I forgot to mention I'm trying to avoid the STL. Sounds like what all of this does is just create small static functions in the background for each array entry that instantiates the specific object type. Maybe I could have a function pointer in the array, each to a separate static function that does just that. – user1054922 Jul 20 '13 at 14:12
  • @user1054922 Correct - the use of the standard library is incidental, I put it there to shorten the lookup. You can definitely replace it with a pair of arrays, the way you did in your post. – Sergey Kalinichenko Jul 20 '13 at 14:18
3

First of all, a syntax primer:

struct Base {
    virtual ~Base() {} // do not forget this if you need polymorphism
};

Then, a "factory" function:

template <typename T>
std::unique_ptr<Base> makeBase() { return std::unique_ptr<Base>(new T{}); }

The type of this function is:

using BaseMaker = std::unique_ptr<Base>(*)();

And finally, putting it altogether:

struct DerivedOne: Base {}; struct DerivedTwo: Base {};

using BaseMakerMap = std::map<std::string, BaseMaker>;

BaseMakerMap const map = { { "DerivedOne", makeBase<DerivedOne> },
                           { "DerivedTwo", makeBase<DerivedTwo> } };

std::unique_ptr<Base> makeFromName(std::string const& n) {
    BaseMakerMap::const_iterator it = map.find(n);

    if (it == map.end()) { return std::unique_ptr<Base>(); } // not found

    BaseMaker maker = it->second;

    return maker();
}
Matthieu M.
  • 287,565
  • 48
  • 449
  • 722
  • This looks like a promising pattern, but it has some compilation errors beyond just fixing the issue with the `map` reference in `makeFromName()`: `error C2207: 'std::pair<_Ty1,_Ty2>::second' : a member of a class template cannot acquire a function type` `error C3867: 'std::pair::second': function call missing argument list; use '&std::pair::second' to create a pointer to member` `error C2064: term does not evaluate to a function taking 0 arguments` – John Kaster Sep 14 '16 at 20:44
  • 1
    @JohnKaster: I fixed the function pointer declaration and the type of `maker`, it now compiles for me on [ideone](https://ideone.com/sjOCO0). – Matthieu M. Sep 15 '16 at 06:44
  • Thanks for the update! I needed something slightly different (a new instance for every time the "name" was encountered which I posted below, but the pattern is valuable. – John Kaster Sep 15 '16 at 20:52
  • 1
    @JohnKaster: The approach I gave also creates a new instance each time the pattern is encountered: the map stores function pointers, not instances, and each invocation of the function creates a new instance. – Matthieu M. Sep 16 '16 at 05:52
  • thanks for sticking with me. I'll update my pattern to use yours since it's more elegant, and follow up after I get it working. – John Kaster Sep 16 '16 at 16:21
  • I got it working with `shared_ptr`, probably because the scope in which I'm using this. with `unique_ptr` I was getting `attempting to reference a deleted function`. Hence, I've deleted my less elegant answer below. Thanks again. – John Kaster Sep 21 '16 at 22:15
  • Would be great to show how to use the makeFromName() function! – MathArt Nov 25 '21 at 09:21
  • @MathArt: I'd hope that anyone can figure out how to call it... it's got a single argument, which needs to match a map entry to do anything useful, there's really no magic there. – Matthieu M. Nov 25 '21 at 09:52
  • It does not return a pointer. – MathArt Nov 26 '21 at 13:34
1

You should be able to do the following:

template<class C>
BaseClass * makeObject<C> () {
    return new C;
}

struct LookupList {
    const char* name;
    BaseClass * (*factoryFunction) ();
};

LookupList list [] = {
    {"ClassOne", &makeObject<DerivedClassOne>},
    {"ClassTwo", &makeObject<DerivedClassTwo>}
};

...

... instantiateFromString ...
    return list[i].factoryFunction ();

I would, however, prefer a Map over an array for the LookupList. In addition, you might want to get familiar with the functional syntax in C++11.

JohnB
  • 13,315
  • 4
  • 38
  • 65
0

You can't initialize your list like this.

typedef struct
{
    const char *name;
    BaseClass class;
} LookupList;

LookupList list[] = {
    {"ClassOne", DerivedClassOne},
    {"ClassTwo", DerivedClassTwo}
};

The list.class is a BaseClass object, while the initialing value is DerivedClassOne, which is a class. That does not make sense. You will get compiler error.

lulyon
  • 6,707
  • 7
  • 32
  • 49