Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Dynamic Dispatch without Virtual Functions

I've got some legacy code that, instead of virtual functions, uses a kind field to do dynamic dispatch. It looks something like this:

// Base struct shared by all subtypes
// Plain-old data; can't use virtual functions
struct POD
{
    int kind;

    int GetFoo();
    int GetBar();
    int GetBaz();
    int GetXyzzy();
};

enum Kind { Kind_Derived1, Kind_Derived2, Kind_Derived3 /* , ... */ };

struct Derived1: POD
{
    Derived1(): kind(Kind_Derived1) {}

    int GetFoo();
    int GetBar();
    int GetBaz();
    int GetXyzzy();

    // ... plus other type-specific data and function members ...
};

struct Derived2: POD
{
    Derived2(): kind(Kind_Derived2) {}

    int GetFoo();
    int GetBar();
    int GetBaz();
    int GetXyzzy();

    // ... plus other type-specific data and function members ...
};

struct Derived3: POD
{
    Derived3(): kind(Kind_Derived3) {}

    int GetFoo();
    int GetBar();
    int GetBaz();
    int GetXyzzy();

    // ... plus other type-specific data and function members ...
};

// ... and so on for other derived classes ...

and then the POD class's function members are implemented like this:

int POD::GetFoo()
{
    // Call kind-specific function
    switch (kind)
    {
    case Kind_Derived1:
        {
        Derived1 *pDerived1 = static_cast<Derived1*>(this);
        return pDerived1->GetFoo();
        }
    case Kind_Derived2:
        {
        Derived2 *pDerived2 = static_cast<Derived2*>(this);
        return pDerived2->GetFoo();
        }
    case Kind_Derived3:
        {
        Derived3 *pDerived3 = static_cast<Derived3*>(this);
        return pDerived3->GetFoo();
        }

    // ... and so on for other derived classes ...

    default:
        throw UnknownKindException(kind, "GetFoo");
    }
}

POD::GetBar(), POD::GetBaz(), POD::GetXyzzy(), and other members are implemented similarly.

This example is simplified. The actual code has about a dozen different subtypes of POD, and a couple dozen methods. New subtypes of POD and new methods are added pretty frequently, and so every time we do that, we have to update all these switch statements.

The typical way to handle this would be to declare the function members virtual in the POD class, but we can't do that because the objects reside in shared memory. There is a lot of code that depends on these structs being plain-old-data, so even if I could figure out some way to have virtual functions in shared-memory objects, I wouldn't want to do that.

So, I'm looking for suggestions as to the best way to clean this up so that all the knowledge of how to call the subtype methods is centralized in one place, rather than scattered among a couple dozen switch statements in a couple dozen functions.

What occurs to me is that I can create some sort of adapter class that wraps a POD and uses templates to minimize the redundancy. But before I start down that path, I'd like to know how others have dealt with this.

like image 232
Kristopher Johnson Avatar asked Jan 14 '11 16:01

Kristopher Johnson


3 Answers

You can use a jump table. This is what most virtual dispatches look like under the hood, and you CAN construct it manually.

template<typename T> int get_derived_foo(POD*ptr) {
    return static_cast<T>(ptr)->GetFoo();
}
int (*)(POD*) funcs[] = {
    get_derived_foo<Derived1>,
    get_derived_foo<Derived2>,
    get_derived_foo<Derived3>
};
int POD::GetFoo() {
    return funcs[kind](this);
}

For a short example.

What exactly are the limitations of being in shared memory? I realized that I don't know enough here. Does it mean that I can't use pointers, because someone in another process will be trying to use those pointers?

You could use a string map, where each process gets it's own copy of the map. You'd have to pass this in to GetFoo() so that it can find it.

struct POD {
    int GetFoo(std::map<int, std::function<int()>& ref) {
        return ref[kind]();
    }
};

Edit: Of course, you don't have to use a string here, you could use an int. I just used it as example. I should change it back. Infact, this solution is pretty flexible, but the important thing is, make a copy of process-specific data, e.g. function pointers or whatever, and then pass it in.

like image 154
Puppy Avatar answered Nov 05 '22 05:11

Puppy


You can experiment with Curiously recurring template pattern. It's a bit complicated, but when you cannot use pure virtual functions it can be helpful.

like image 1
Goofy Avatar answered Nov 05 '22 04:11

Goofy


Here is an approach that uses virtual methods to implement the jump table, without requiring the Pod class or the derived classes to actually have virtual functions.

The objective is to simplify adding and removing methods across many classes.

To add a method, it needs to be added to Pod using a clear and common pattern, a pure virtual function needs to be added to PodInterface, and a forwarding function must be added to PodFuncs using a clear and common pattern.

Derived classes need only have a file static initialisation object to set things up, otherwise look pretty much like they already do.

// Pod header

#include <boost/shared_ptr.hpp>
enum Kind { Kind_Derived1, Kind_Derived2, Kind_Derived3 /* , ... */ };

struct Pod
{
    int kind;

    int GetFoo();
    int GetBar();
    int GetBaz();
};

struct PodInterface
{
    virtual ~PodInterface();

    virtual int GetFoo(Pod* p) const = 0;
    virtual int GetBar(Pod* p) const = 0;
    virtual int GetBaz(Pod* p) const = 0;

    static void
    do_init(
            boost::shared_ptr<PodInterface const> const& p,
            int kind);
};

template<class T> struct PodFuncs : public PodInterface
{
    struct Init
    {
        Init(int kind)
        {
            boost::shared_ptr<PodInterface> t(new PodFuncs);
            PodInterface::do_init(t, kind);
        }
    };

    ~PodFuncs() { }

    int GetFoo(Pod* p) const { return static_cast<T*>(p)->GetFoo(); }
    int GetBar(Pod* p) const { return static_cast<T*>(p)->GetBar(); }
    int GetBaz(Pod* p) const { return static_cast<T*>(p)->GetBaz(); }
};


//
// Pod Implementation
//

#include <map>

typedef std::map<int, boost::shared_ptr<PodInterface const> > FuncMap;

static FuncMap& get_funcmap()
{
    // Replace with other approach for static initialisation order as appropriate.
    static FuncMap s_funcmap;
    return s_funcmap;
}

//
// struct Pod methods
//

int Pod::GetFoo()
{
    return get_funcmap()[kind]->GetFoo(this);
}

//
// struct PodInterface methods, in same file as s_funcs
//

PodInterface::~PodInterface()
{
}

void
PodInterface::do_init(
        boost::shared_ptr<PodInterface const> const& p,
        int kind)
{
    // Could do checking for duplicates here.
    get_funcmap()[kind] = p;
}

//
// Derived1
//

struct Derived1 : Pod
{
    Derived1() { kind = Kind_Derived1; }

    int GetFoo();
    int GetBar();
    int GetBaz();

    // Whatever else.
};

//
// Derived1 implementation
//

static const PodFuncs<Derived1>::Init s_interface_init(Kind_Derived1);

int Derived1::GetFoo() { /* Implement */ }
int Derived1::GetBar() { /* Implement */ }
int Derived1::GetBaz() { /* Implement */ } 
like image 1
janm Avatar answered Nov 05 '22 05:11

janm