Из-за моего устройства я не могу использовать виртуальные функции. Предположим, что у меня есть:
Ответ 4
Мой первый ответ показывает, что действительно возможно получить хотя бы ограниченную форму полиморфного поведения, фактически не полагаясь на поддержку языка для полиморфизма.
Однако этот пример имеет огромное количество шаблонов. Это, конечно, не будет хорошо масштабироваться: для каждого добавляемого вами класса вам нужно изменить шесть разных мест в коде, а для каждой функции-члена, которую вы хотите поддерживать, вам нужно дублировать большую часть этого кода. Тьфу.
Ну, хорошие новости: с помощью препроцессора (и библиотеки Boost.Preprocessor, конечно), мы сможем легко извлечь большую часть этого boilderplate и сделать это решение управляемым.
Чтобы получить шаблонный шаблон, вам понадобятся эти макросы. Вы можете поместить их в файл заголовка и забыть о них, если хотите; они довольно общие. [Пожалуйста, не убегайте, прочитав это; если вы не знакомы с библиотекой Boost.Preprocessor, это, вероятно, выглядит ужасно:-) После этого первого блока кода мы увидим, как мы можем использовать это, чтобы сделать наш код приложения намного чище. Если вы хотите, вы можете просто игнорировать детали этого кода.]
Код представлен в порядке, потому что, если вы скопируете и пропустите каждый из блоков кода из этого сообщения, в порядке, в исходный файл С++, он будет (я имею в виду, должен!) компилироваться и запускаться.
Я назвал это "Псевдополиморфной библиотекой"; любые имена, начинающиеся с "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)]
Ответ 6
Поскольку виртуальные методы обычно реализуются с помощью vtables, нет никакой магии, которая не может быть реплицирована в коде. Фактически вы могли бы реализовать свой собственный механизм виртуальной отправки. Требуется некоторая работа, как со стороны программиста, реализующего базовый класс, так и программиста, который реализует производный класс, но он работает.
Литье указателя, как и было предложено ceretullis, вероятно, первое, что вам следует рассмотреть. Но решение, которое я размещаю здесь, по крайней мере, дает вам возможность писать код, который использует эти классы, как если бы ваш компилятор поддерживал virtual
. То есть, с простым вызовом функции.
Это программа, которая реализует класс Base
с функцией, которая возвращает string
: "base" и класс Derived
, который возвращает string
: "der". Идея заключается в том, чтобы поддерживать такой код:
Base* obj = new Der;
cout << obj->get_string();
... так что вызов get_string()
будет возвращать "der", даже если мы вызываем указатель Base
и используя компилятор, который не поддерживает virtual
.
Он работает, реализуя нашу собственную версию vtable. Собственно, это не настоящая таблица. Это просто указатель функции-члена в базовом классе. В реализации базового класса get_string()
, если указатель на функцию-член не равен нулю, вызывается функция. Если он равен нулю, выполняется реализация базового класса.
Простой, понятный и довольно простой. Вероятно, это может быть значительно улучшено. Но он показывает основную технику.
#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;
}