Могу ли я получить полиморфное поведение без использования виртуальных функций?
Из-за моего устройства я не могу использовать виртуальные функции. Предположим, у меня есть:
class Base
{
void doSomething() { }
};
class Derived : public Base
{
void doSomething() { }
};
// in any place
{
Base *obj = new Derived;
obj->doSomething();
}
obj->doSomething()
назову только Base::doSomething()
Есть ли способ с Base *obj
, чтобы позвонить doSomething
из Derived
?
Я знаю, я могу просто поставить virtual
до doSomething()
из Base
это решает проблему, но я ограничен моим устройством, компилятор не поддерживает его.
10 ответов
Вы можете привести указатель базового класса к производному классу и вызвать функцию.
Base* obj = new Derived;
Derived* d = static_cast<Derived*>( obj );
d->doSomething();
поскольку doSomething()
не объявлено virtual
, вы должны получить производную реализацию.
Конечно, вы можете сделать это; это просто не обязательно легко.
Если существует конечный список производных классов, и вы знаете, что это такое, когда определяете базовый класс, вы можете сделать это, используя неполиморфную оболочку функции-члена. Вот пример с двумя производными классами. Он не использует стандартные библиотечные средства и опирается исключительно на стандартные функции C++.
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()
}
(Я изначально предложил использовать std::function
, который делает большую часть этой работы для вас, но потом я вспомнил, что это оболочка полиморфной функции, поэтому он обязательно использует виртуальные функции. Ой. Вы можете просмотреть историю изменений, чтобы увидеть, как это выглядит)
Мой первый ответ показывает, что действительно возможно получить хотя бы ограниченную форму полиморфно-подобного поведения, фактически не полагаясь на поддержку языка полиморфизма.
Тем не менее, этот пример имеет огромное количество шаблонов. Это, конечно, не будет хорошо масштабироваться: для каждого добавляемого вами класса вам нужно изменить шесть разных мест в коде, а для каждой функции-члена, которую вы хотите поддерживать, вам нужно дублировать большую часть этого кода. Тьфу.
Что ж, хорошие новости: с помощью препроцессора (и, конечно, библиотеки Boost.Preprocessor) мы можем легко извлечь большую часть этой таблицы и сделать это решение управляемым.
Чтобы убрать шаблон с пути, вам понадобятся эти макросы. Вы можете поместить их в заголовочный файл и забыть о них, если хотите; они довольно общие. [Пожалуйста, не убегайте после прочтения этого; Если вы не знакомы с библиотекой Boost.Preprocessor, она, вероятно, выглядит ужасно:-) После этого первого блока кода мы увидим, как мы можем использовать это, чтобы сделать код нашего приложения намного чище. Если вы хотите, вы можете просто игнорировать детали этого кода.]
Код представлен в том порядке, в каком он есть, потому что если вы скопируете и вставите каждый из блоков кода из этого поста, по порядку, в исходный файл C++, он будет (я имею в виду, должен!) Компилироваться и запускаться.
Я назвал это "псевдополиморфной библиотекой"; любые имена, начинающиеся с "PseudoPM" с любой заглавной буквы, должны рассматриваться как зарезервированные им. Макросы, начинающиеся с PSEUDOPM
являются публично вызываемыми макросами; макросы, начинающиеся с PSEUDOPMX
для внутреннего использования.
#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)
(Мы должны сделать vtable статическим, но я оставлю это в качестве упражнения для читателя.:-D)
Теперь, когда это не так, мы можем посмотреть, что вам нужно сделать в вашем приложении, чтобы использовать это.
Во-первых, нам нужно определить список классов, которые будут в нашей иерархии классов:
// 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)
Во-вторых, нам нужно определить список "виртуальных" функций-членов. Обратите внимание, что в этой (предположительно ограниченной) реализации базовый класс и каждый производный класс должны реализовывать каждую из "виртуальных" функций-членов. Если класс не определит один из них, компилятор рассердится.
// 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)))
Обратите внимание, что вы можете назвать эти два макроса как угодно; вам просто нужно обновить ссылки в следующих фрагментах.
Далее мы можем определить наши классы. В базовом классе нам нужно позвонить PSEUDOPM_DECLARE_VTABLE
объявить функции виртуального члена и определить весь шаблон для нас. Во всех наших конструкторах классов нам нужно вызывать PSEUDOPM_INIT_VTABLE
; этот макрос генерирует код, необходимый для правильной инициализации vtable.
В каждом классе мы также должны определить все функции-члены, перечисленные выше в VIRTUAL_FUNCTIONS
последовательность. Обратите внимание, что нам нужно назвать реализации с Impl
суффикс; это потому, что реализации всегда вызываются через функции диспетчера, которые генерируются PSEUDOPM_DECLARE_VTABLE
макро.
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); }
};
Наконец, в некотором исходном файле вам нужно будет включить все заголовки, в которых определены все классы, и вызвать PSEUDOPM_DEFINE_VTABLE
макро; этот макрос фактически определяет функции диспетчера. Этот макрос не может использоваться, если все классы еще не определены (он должен static_cast
базовый класс this
указатель, и это не удастся, если компилятор не знает, что производный класс на самом деле является производным от базового класса).
PSEUDOPM_DEFINE_VTABLE(CLASSES, VIRTUAL_FUNCTIONS)
Вот некоторый тестовый код, который демонстрирует функциональность:
#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
}
[Отказ от ответственности: этот код проверен только частично. Может содержать ошибки. (На самом деле, вероятно, так и есть; большую часть времени я написал в 1 час утра:-P)]
Вы можете понизить объект до производного типа и вызвать его, например, так:
static_cast<Derived*>(obj)->doSomething();
хотя это не дает никаких гарантий, что то, на что указывает 'obj', действительно имеет тип Derived
,
Меня больше беспокоит, что у вас даже нет доступа к виртуальным функциям. Как работают деструкторы, если ни одна из ваших функций не может быть виртуальной, а вы создаете подклассы?
Поскольку виртуальные методы обычно реализуются с помощью vtables, не происходит никакого волшебства, которое вы не можете воспроизвести в коде. Фактически вы могли бы реализовать свой собственный механизм виртуальной отправки. Требуется некоторая работа, как со стороны программиста, который реализует базовый класс, так и программиста, который реализует производный класс, но это работает.
Приведение указателя, как предложено ceretullis, вероятно, первое, что вы должны рассмотреть. Но решение, которое я выкладываю здесь, по крайней мере, дает вам возможность написать код, который использует эти классы, как если бы ваш компилятор поддерживал virtual
, То есть с простым вызовом функции.
Это программа, которая реализует Base
класс с функцией, которая возвращает string
: "база" и Derived
класс, который возвращает string
: "дер". Идея состоит в том, чтобы иметь возможность поддерживать такой код:
Base* obj = new Der;
cout << obj->get_string();
...таким образом get_string()
вызов вернет "дер", хотя мы звоним через Base
указатель и использование компилятора, который не поддерживает virtual
,
Он работает путем реализации нашей собственной версии vtable. На самом деле, это не совсем стол. Это просто указатель на функцию-член в базовом классе. В базовом классе 'реализация get_string()
, если указатель на функцию-член не равен NULL, функция вызывается. Если это нуль, реализация базового класса выполняется.
Простой, простой и довольно простой. Это, вероятно, могло бы быть значительно улучшено. Но это показывает основную технику.
#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;
}
Вы можете сделать свой собственный vtable, я полагаю. Я просто был бы структурой, содержащей ваши "виртуальные" указатели на функции как часть Base, и имел бы код для настройки vtable.
Это своего рода грубое решение - работа компилятора C++ для обработки этой функции.
Но здесь идет:
#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;}
};
Можете ли вы инкапсулировать базовый класс, а не наследовать от него?
Затем вы можете вызвать doSomething() // получает производные
или base->doSomething() // вызывает базу
Вы можете использовать шаблон для полиморфизма во время компиляции.
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;
Теперь мы не можем указать на Derived
с Base
указатель, но мы можем иметь шаблонные функции, которые принимают MyClass, и это будет работать как с Base
а также Derived
объекты.
Я думаю, что это возможно с CRTP (если ваше "Устройство" поддерживает шаблоны).
#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();
}
Небольшая программа для понимания, мы можем использовать static_cast для приведения указателя базового класса к производному классу и вызова функции.
#include<iostream>
using namespace std;
class Base
{
public:
void display()
{
cout<<"From Base class\n";
}
};
class Derived:public Base
{
public:
void display()
{
cout<<"From Derived class";
}
};
int main()
{
Base *ptr=new Derived;
Derived* d = static_cast<Derived*>(ptr);
ptr->display();
d->display();
return 0;
}
Выход:
Из базового класса Из производного класса