My first answer shows that it is indeed possible to get at least a limited form of polymorphic-like behavior without actually relying on the language's support for polymorphism.
However, that example has an enormous amount of boilerplate. It certainly wouldn't scale well: for every class that you add you have to modify six different places in the code, and for every member function that you want to support, you need to duplicate most of that code. Yuck.
Well, good news: with the help of the preprocessor (and the Boost.Preprocessor library, of course), we can easily extract most of that boilderplate and make this solution manageable.
To get the boilerplate out of the way, you'll need these macros. You can put them in a header file and forget about them if you want; they are fairly generic. [Please don't run away after reading this; if you aren't familiar with the Boost.Preprocessor library, it probably looks terrifying :-) After this first code block, we'll see how we can use this to make our application code a lot cleaner. If you want, you can just ignore the details of this code.]
The code is presented in the order it is because if you copy and past each of the code blocks from this post, in order, into a C++ source file, it will (I mean should!) compile and run.
I've called this the "Pseudo-Polymorphic Library;" any names beginning with "PseudoPM," with any capitalization, should be considered reserved by it. Macros beginning with PSEUDOPM
are publicly callable macros; macros beginning with PSEUDOPMX
are for internal use.
#include <boost/preprocessor.hpp>
// [INTERNAL] PSEUDOPM_INIT_VTABLE Support
#define PSEUDOPMX_INIT_VTABLE_ENTRY(r, c, i, fn) \
BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i)) \
& c :: BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Impl)
// [INTERNAL] PSEUDOPM_DECLARE_VTABLE Support
#define PSEUDOPMX_DECLARE_VTABLE_STRUCT_MEMBER(r, c, i, fn) \
BOOST_PP_TUPLE_ELEM(4, 1, fn) \
(c :: * BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Ptr)) \
BOOST_PP_TUPLE_ELEM(4, 3, fn);
#define PSEUDOPMX_DECLARE_VTABLE_STRUCT(r, memfns, c) \
struct BOOST_PP_CAT(PseudoPMIntVTable, c) \
{ \
friend class c; \
BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DECLARE_VTABLE_STRUCT_MEMBER, c, memfns)\
};
#define PSEUDOPMX_DECLARE_VTABLE_ENUM_MEMBER(r, x, i, c) \
BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i)) BOOST_PP_CAT(PseudoPMType, c)
#define PSEUDOPMX_DECLARE_VTABLE_UNION_MEMBER(r, x, c) \
BOOST_PP_CAT(PseudoPMIntVTable, c) BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _);
#define PSEUDOPMX_DECLARE_VTABLE_RESET_FN(r, x, c) \
void Reset(BOOST_PP_CAT(PseudoPMIntVTable, c) table) \
{ \
type_ = BOOST_PP_CAT(PseudoPMType, c); \
table_.BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _) = table; \
}
#define PSEUDOPMX_DECLARE_VTABLE_PUBLIC_FN(r, x, fn) \
BOOST_PP_TUPLE_ELEM(4, 1, fn) \
BOOST_PP_TUPLE_ELEM(4, 0, fn) \
BOOST_PP_TUPLE_ELEM(4, 3, fn);
// [INTERNAL] PSEUDOPM_DEFINE_VTABLE Support
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST0
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST1 a0
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST2 a0, a1
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST3 a0, a1, a2
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST4 a0, a1, a2, a3
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST5 a0, a1, a2, a3, a4
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST6 a0, a1, a2, a3, a4, a5
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST7 a0, a1, a2, a3, a4, a5, a6
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST8 a0, a1, a2, a3, a4, a5, a6, a7
#define PSEUDOPMX_DEFINE_VTABLE_ARGLIST9 a0, a1, a2, a3, a4, a5, a6, a7, a8
#define PSEUDOPMX_DEFINE_VTABLE_FNP(r, x, i, t) \
BOOST_PP_COMMA_IF(BOOST_PP_NOT_EQUAL(0, i)) \
t BOOST_PP_CAT(a, i)
#define PSEUDOPMX_DEFINE_VTABLE_FN_CASE(r, fn, i, c) \
case BOOST_PP_CAT(PseudoPMType, c) : return \
( \
static_cast<c*>(this)->*pseudopm_vtable_.table_. \
BOOST_PP_CAT(BOOST_PP_CAT(table_, c), _). \
BOOST_PP_CAT(BOOST_PP_TUPLE_ELEM(4, 0, fn), Ptr) \
)( \
BOOST_PP_CAT( \
PSEUDOPMX_DEFINE_VTABLE_ARGLIST, \
BOOST_PP_TUPLE_ELEM(4, 2, fn) \
) \
);
#define PSEUDOPMX_DEFINE_VTABLE_FN(r, classes, fn) \
BOOST_PP_TUPLE_ELEM(4, 1, fn) \
BOOST_PP_SEQ_HEAD(classes) :: BOOST_PP_TUPLE_ELEM(4, 0, fn) \
( \
BOOST_PP_SEQ_FOR_EACH_I( \
PSEUDOPMX_DEFINE_VTABLE_FNP, x, \
BOOST_PP_TUPLE_TO_SEQ( \
BOOST_PP_TUPLE_ELEM(4, 2, fn), \
BOOST_PP_TUPLE_ELEM(4, 3, fn) \
) \
) \
) \
{ \
switch (pseudopm_vtable_.type_) \
{ \
BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DEFINE_VTABLE_FN_CASE, fn, classes) \
} \
}
// Each class in the classes sequence should call this macro at the very
// beginning of its constructor. 'c' is the name of the class for which
// to initialize the vtable, and 'memfns' is the member function sequence.
#define PSEUDOPM_INIT_VTABLE(c, memfns) \
BOOST_PP_CAT(PseudoPMIntVTable, c) pseudopm_table = \
{ \
BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_INIT_VTABLE_ENTRY, c, memfns) \
}; \
pseudopm_vtable_.Reset(pseudopm_table);
// The base class should call this macro in its definition (at class scope).
// This defines the virtual table structs, enumerations, internal functions,
// and declares the public member functions. 'classes' is the sequence of
// classes and 'memfns' is the member function sequence.
#define PSEUDOPM_DECLARE_VTABLE(classes, memfns) \
protected: \
BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_STRUCT, memfns, classes) \
\
enum PseudoPMTypeEnum \
{ \
BOOST_PP_SEQ_FOR_EACH_I(PSEUDOPMX_DECLARE_VTABLE_ENUM_MEMBER, x, classes) \
}; \
\
union PseudoPMVTableUnion \
{ \
BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_UNION_MEMBER, x, classes) \
}; \
\
class PseudoPMVTable \
{ \
public: \
BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_RESET_FN, x, classes) \
private: \
friend class BOOST_PP_SEQ_HEAD(classes); \
PseudoPMTypeEnum type_; \
PseudoPMVTableUnion table_; \
}; \
\
PseudoPMVTable pseudopm_vtable_; \
\
public: \
BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DECLARE_VTABLE_PUBLIC_FN, x, memfns)
// This macro must be called in some source file after all of the classes in
// the classes sequence have been defined (so, for example, you can create a
// .cpp file, include all the class headers, and then call this macro. It
// actually defines the public member functions for the base class. Each of
// the public member functions calls the correct member function in the
// derived class. 'classes' is the sequence of classes and 'memfns' is the
// member function sequence.
#define PSEUDOPM_DEFINE_VTABLE(classes, memfns) \
BOOST_PP_SEQ_FOR_EACH(PSEUDOPMX_DEFINE_VTABLE_FN, classes, memfns)
(We should make the vtable static, but I'll leave that as an excercise for the reader. :-D)
Now that that is out of the way, we can actually look at what you need to do in your application to use this.
First, we need to define the list of classes that are going to be in our class hierarchy:
// The sequence of classes in the class hierarchy. The base class must be the
// first class in the sequence. Derived classes can be in any order.
#define CLASSES (Base)(Derived)
Second, we need to define the list of "virtual" member functions. Note that with this (admittedly limited) implementation, the base class and every derived class must implement every one of the "virtual" member functions. If a class doesn't define one of these, the compiler will get angry.
// The sequence of "virtual" member functions. Each entry in the sequence is a
// four-element tuple:
// (1) The name of the function. A function will be declared in the Base class
// with this name; it will do the dispatch. All of the classes in the class
// sequence must implement a private implementation function with the same
// name, but with "Impl" appended to it (so, if you declare a function here
// named "Foo" then each class must define a "FooImpl" function.
// (2) The return type of the function.
// (3) The number of arguments the function takes (arity).
// (4) The arguments tuple. Its arity must match the number specified in (3).
#define VIRTUAL_FUNCTIONS \
((FuncNoArg, void, 0, ())) \
((FuncOneArg, int, 1, (int))) \
((FuncTwoArg, int, 2, (int, int)))
Note that you can name these two macros whatever you want; you'll just have to update the references in the following snippets.
Next, we can define our classes. In the base class, we need to call PSEUDOPM_DECLARE_VTABLE
to declare the virtual member functions and define all the boilerplate for us. In all of our class constructors, we need to call PSEUDOPM_INIT_VTABLE
; this macro generates the code required to initialize the vtable correctly.
In each class we must also define all of the member functions we listed above in the VIRTUAL_FUNCTIONS
sequence. Note that we need to name the implementations with an Impl
suffix; this is because the implementations are always called through the dispatcher functions that are generated by the PSEUDOPM_DECLARE_VTABLE
macro.
class Base
{
public:
Base()
{
PSEUDOPM_INIT_VTABLE(Base, VIRTUAL_FUNCTIONS)
}
PSEUDOPM_DECLARE_VTABLE(CLASSES, VIRTUAL_FUNCTIONS)
private:
void FuncNoArgImpl() { }
int FuncOneArgImpl(int x) { return x; }
int FuncTwoArgImpl(int x, int y) { return x + y; }
};
class Derived : public Base
{
public:
Derived()
{
PSEUDOPM_INIT_VTABLE(Derived, VIRTUAL_FUNCTIONS)
}
private:
void FuncNoArgImpl() { }
int FuncOneArgImpl(int x) { return 2 * x; }
int FuncTwoArgImpl(int x, int y) { return 2 * (x + y); }
};
Finally, in some source file, you'll need to include all the headers where all the classes are defined and call the PSEUDOPM_DEFINE_VTABLE
macro; this macro actually defines the dispatcher functions. This macro cannot be used if all of the classes have not yet been defined (it has to static_cast
the base class this
pointer, and this will fail if the compiler doesn't know that the derived class is actually derived from the base class).
PSEUDOPM_DEFINE_VTABLE(CLASSES, VIRTUAL_FUNCTIONS)
Here is some test code that demonstrates the functionality:
#include <cassert>
int main()
{
Base* obj0 = new Base;
Base* obj1 = new Derived;
obj0->FuncNoArg(); // calls Base::FuncNoArg
obj1->FuncNoArg(); // calls Derived::FuncNoArg
assert(obj0->FuncTwoArg(2, 10) == 12); // Calls Base::FuncTwoArg
assert(obj1->FuncTwoArg(2, 10) == 24); // Calls Derived::FuncTwoArg
}
[Disclaimer: This code is only partially tested. It may contain bugs. (In fact, it probably does; I wrote most of it at 1 am this morning :-P)]
Since virtual methods are usually implemented by means of vtables, there's no magic happening that you can't replicate in code. You could, in fact, implement your own virtual dispatch mechanism. It takes some work, both on the part of the programmer who implements the base class and the programmer who implements the derived class, but it works.
Casting the pointer, like as suggested by ceretullis, is probably the first thing you should consider doing. But the solution I post here at least gives you the opportunity to write code that uses these classes as if your compiler supported virtual
. That is, with a simple function call.
This is a program that implements a Base
class with a function that returns a string
: "base", and a Derived
class that returns a string
: "der". The idea is to be able to support code like this:
Base* obj = new Der;
cout << obj->get_string();
...so that the get_string()
call will return "der" even though we are calling through a Base
pointer and using a compiler that doesn't support virtual
.
It works by implementing our own version of a vtable. Actually, it's not really a table. It's just a member-function pointer in the base class. In the base class' implementation of get_string()
, if the member-function pointer is non-null, the function is called. If it is null, the base class implementation is executed.
Simple, straightforward and pretty basic. This could probably be improved a lot. But it shows the basic technique.
#include <cstdlib>
#include <string>
#include <iostream>
using namespace std;
class Base
{
public:
typedef string (Base::*vptr_get_string)(void) const;
Base(vptr_get_string=0);
void set_derived_pointer(Base* derived);
string get_string() const;
protected:
Base* der_ptr_;
vptr_get_string get_string_vf_;
};
Base::Base(vptr_get_string get_string_vf)
: der_ptr_(0),
get_string_vf_(get_string_vf)
{
}
void Base::set_derived_pointer(Base* derived)
{
der_ptr_ = derived;
}
string Base::get_string() const
{
if( get_string_vf_ )
return (der_ptr_->*get_string_vf_)();
else
return "base";
}
class Der : public Base
{
public:
Der();
string get_string() const;
};
Der::Der()
: Base(static_cast<Base::vptr_get_string>(&Der::get_string))
{
set_derived_pointer(this);
}
string Der::get_string() const
{
return "der";
}
int main()
{
Base* obj = new Der;
cout << obj->get_string();
delete obj;
}