Can I get polymorphic behavior without using virtu

2019-01-11 23:07发布

问题:

Because of my device I can't use virtual functions. Suppose I have:

class Base
{
    void doSomething() { }
};

class Derived : public Base
{
    void doSomething() { }
};

// in any place
{
    Base *obj = new Derived;
    obj->doSomething();
}

the obj->doSomething() will call just the Base::doSomething()

Is there a way with Base *obj, to call the doSomething of the Derived?

I know I can just put a virtual before doSomething() of Base it solve the problem, but I'm limited by my device, the compiler doesn't support it.

回答1:

You could down cast the base class pointer to the derived class and call the function.

Base* obj = new Derived;
Derived* d = static_cast<Derived*>( obj ); 
d->doSomething();

Since doSomething() is not declared virtual, you should get the derived implementation.



回答2:

Sure you can do this; it's just not necessarily easy.

If there is a finite list of derived classes and you know what they are when you define the base class, you can do this using a non-polymorphic member function wrapper. Here is an example with two derived classes. It uses no standard library facilities and relies solely on standard C++ features.

class Base;
class Derived1;
class Derived2;

class MemFnWrapper
{
public:

    enum DerivedType { BaseType, Derived1Type, Derived2Type };

    typedef void(Base::*BaseFnType)();
    typedef void(Derived1::*Derived1FnType)();
    typedef void(Derived2::*Derived2FnType)();

    MemFnWrapper(BaseFnType fn) : type_(BaseType) { fn_.baseFn_ = fn; }
    MemFnWrapper(Derived1FnType fn) : type_(Derived1Type) {fn_.derived1Fn_ = fn;}
    MemFnWrapper(Derived2FnType fn) : type_(Derived2Type) {fn_.derived2Fn_ = fn;}

    void operator()(Base* ptr) const;

private:

    union FnUnion
    {
        BaseFnType baseFn_;
        Derived1FnType derived1Fn_;
        Derived2FnType derived2Fn_;
    };

    DerivedType type_;
    FnUnion fn_;
};

class Base
{
public:

    Base() : doSomethingImpl(&Base::myDoSomething) { }
    Base(MemFnWrapper::Derived1FnType f) : doSomethingImpl(f) { }
    Base(MemFnWrapper::Derived2FnType f) : doSomethingImpl(f) { }

    void doSomething() { doSomethingImpl(this); }
private:
    void myDoSomething() { }
    MemFnWrapper doSomethingImpl;
};

class Derived1 : public Base
{
public:
    Derived1() : Base(&Derived1::myDoSomething) { }
private:
    void myDoSomething() { } 
};

class Derived2 : public Base
{
public:
    Derived2() : Base(&Derived2::myDoSomething) { }
private:
    void myDoSomething() { } 
};

// Complete the MemFnWrapper function call operator; this has to be after the
// definitions of Derived1 and Derived2 so the cast is valid:
void MemFnWrapper::operator()(Base* ptr) const
{
    switch (type_)
    {
    case BaseType:     return (ptr->*(fn_.baseFn_))();
    case Derived1Type: return (static_cast<Derived1*>(ptr)->*(fn_.derived1Fn_))();
    case Derived2Type: return (static_cast<Derived2*>(ptr)->*(fn_.derived2Fn_))();
    }
}

int main()
{
    Base* obj0 = new Base;
    Base* obj1 = new Derived1;
    Base* obj2 = new Derived2;
    obj0->doSomething(); // calls Base::myDoSomething()
    obj1->doSomething(); // calls Derived1::myDoSomething()
    obj2->doSomething(); // calls Derived2::myDoSomething()
}

(I originally suggested using std::function, which does a lot of this work for you, but then I remembered it is a polymorphic function wrapper, so it necessarily uses virtual functions. :-P Oops. You can view the revision history to see what that one looked like)



回答3:

You can downcast the object to the Derived type and call it, like so:

static_cast<Derived*>(obj)->doSomething();

though that does not afford any guarantees that what 'obj' points to really is of type Derived.

I'm more concerned that you don't even have access to virtual functions. How do destructors work if none of your functions can be virtual, and you are subclassing?



回答4:

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)]



回答5:

You can make your own vtable, I suppose. I'd just be a struct containing your "virtual" function pointers as part of Base, and have code to set up the vtable.

This is kind of a gross solution -- it's the C++ compiler's job to handle this feature.

But here goes:

#include <iostream>

class Base
{
protected:
    struct vt {
        void (*vDoSomething)(void);
    } vt;
private:
    void doSomethingImpl(void) { std::cout << "Base doSomething" << std::endl; }
public:
    void doSomething(void) { (vt.vDoSomething)();}
    Base() : vt() { vt.vDoSomething = (void(*)(void)) &Base::doSomethingImpl;}
};

class Derived : public Base
{
public:
    void doSomething(void) { std::cout << "Derived doSomething" << std::endl; }
    Derived() : Base() { vt.vDoSomething = (void(*)(void)) &Derived::doSomething;}
};


回答6:

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;
}


回答7:

Can you encapsulate the base class rather than deriving from it?

Then you can call doSomething() // gets derived
or base->doSomething() // calls base



回答8:

You can use template for compile-time polymorphism.

template<class SomethingDoer> class MyClass
{
    public:
        void doSomething() {myDoer.doSomething();}
    private:
        SomethingDoer myDoer;
};

class BaseSomethingDoer
{
    public:
        void doSomething() { // base implementation }
};

class DerivedSomethingDoer
{
    public:
        void doSomething() { // derived implementation }
};

typedef MyClass<BaseSomethingDoer> Base;
typedef MyClass<DerivedSomethingDoer> Derived;

Now, we can't point to a Derived with a Base pointer, but we can have templated functions that take in a MyClass, and that will work with both Base and Derived objects.



回答9:

There is simply no simple way to do this without virtual methods.



回答10:

I think it is possible with CRTP (if your 'Device' supports Templates).

#include <iostream>

template<class T> struct base{
    void g(){
        if(T *p = static_cast<T *>(this)){
            p->f();
        }
    }
    void f(){volatile int v = 0; std::cout << 1;}
    virtual ~base(){}
};

struct derived1 : base<derived1>{
    void f(){std::cout << 2;}
};

struct derived2 : base<derived2>{
    void f(){std::cout << 3;}
};

int main(){
    derived1 d1;
    d1.g();

    derived2 d2;
    d2.g();
}