Динамическая отправка С++ без виртуальных функций
У меня есть устаревший код, который вместо виртуальных функций использует поле kind
для динамической отправки. Это выглядит примерно так:
// 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 ...
а затем члены функции класса POD
реализованы следующим образом:
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()
, и другие члены реализованы аналогично.
Этот пример упрощен. Фактический код имеет около десятка разных подтипов POD
и несколько десятков методов. Новые подтипы POD
и новые методы добавляются довольно часто, поэтому каждый раз, когда мы это делаем, мы должны обновлять все эти операторы switch
.
Типичным способом обработки этого было бы объявить членов функции virtual
в классе POD
, но мы не можем этого сделать, потому что объекты находятся в общей памяти. Существует много кода, который зависит от того, какие эти структуры являются обычными данными, поэтому, даже если бы я мог найти способ иметь виртуальные функции в объектах с общей памятью, я бы не хотел этого делать.
Итак, я ищу предложения о том, как наилучшим образом очистить это, чтобы все знания о том, как вызвать методы подтипа, централизованы в одном месте, а не разбросаны между несколькими дюжинами switch
операторов в пара дюжины функций.
Что происходит со мной, так это то, что я могу создать какой-то класс адаптера, который обертывает POD
и использует шаблоны для минимизации избыточности. Но прежде чем я начну с этого пути, я хотел бы знать, как другие справились с этим.
Ответы
Ответ 1
Вы можете использовать таблицу перехода. Это то, что большинство виртуальных рассылок выглядят под капотом, и вы МОЖЕТЕ построить его вручную.
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);
}
Для краткого примера.
В чем конкретно заключаются ограничения в общей памяти? Я понял, что здесь не знаю. Означает ли это, что я не могу использовать указатели, потому что кто-то из другого процесса будет пытаться использовать эти указатели?
Вы можете использовать строковую карту, где каждый процесс получает свою собственную копию карты. Вам нужно передать это в GetFoo(), чтобы он мог его найти.
struct POD {
int GetFoo(std::map<int, std::function<int()>& ref) {
return ref[kind]();
}
};
Изменить: Конечно, вам не нужно использовать строку здесь, вы можете использовать int. Я просто использовал его в качестве примера. Я должен изменить его. Infact, это решение довольно гибкое, но важно то, что сделайте копию данных, относящихся к конкретному процессу, например. указатели функций или что-то еще, а затем передать его.
Ответ 2
Вы можете поэкспериментировать с Любопытно повторяющийся шаблон шаблона. Это немного сложно, но если вы не можете использовать чистые виртуальные функции, это может быть полезно.
Ответ 3
Вот подход, который использует виртуальные методы для реализации таблицы перехода, не требуя, чтобы класс Pod или производные классы фактически имели виртуальные функции.
Цель состоит в том, чтобы упростить добавление и удаление методов во многих классах.
Чтобы добавить метод, его необходимо добавить в Pod с использованием четкого и общего шаблона, в PodInterface необходимо добавить чистую виртуальную функцию, а функция пересылки должна быть добавлена в PodFunc с использованием четкого и общего шаблона.
Производные классы нуждаются только в объекте статической инициализации файла для настройки, в противном случае они выглядят так же, как они уже делают.
// 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 */ }
Ответ 4
Здесь путь шаблона-метапрограммирования, о котором я сейчас иду. Вот что мне нравится:
- Добавление поддержки для нового типа требует обновления
LAST_KIND
и добавления нового KindTraits
.
- Существует простой шаблон для добавления новой функции.
- При необходимости функции могут быть специализированы для определенных видов.
- Я могу ожидать ошибок и предупреждений во время компиляции, а не таинственного неправильного поведения во время выполнения, если я все испортил.
Есть несколько проблем:
-
POD
реализация теперь зависит от интерфейсов всех производных классов. (Это уже верно в существующей реализации, поэтому я не беспокоюсь об этом, но это немного запах.)
- Я рассчитываю, что компилятор будет достаточно умным, чтобы генерировать код, который примерно эквивалентен коду
switch
.
- Многие программисты на C++ поцарапают свои головы, увидев это.
Здесь код:
// Declare first and last kinds
const int FIRST_KIND = Kind_Derived1;
const int LAST_KIND = Kind_Derived3;
// Provide a compile-time mapping from a kind code to a subtype
template <int KIND>
struct KindTraits
{
typedef void Subtype;
};
template <> KindTraits<Kind_Derived1> { typedef Derived1 Subtype; };
template <> KindTraits<Kind_Derived2> { typedef Derived2 Subtype; };
template <> KindTraits<Kind_Derived3> { typedef Derived3 Subtype; };
// If kind matches, then do the appropriate typecast and return result;
// otherwise, try the next kind.
template <int KIND>
int GetFooForKind(POD *pod)
{
if (pod->kind == KIND)
return static_cast<KindTraits<KIND>::Subtype>(pod)->GetFoo();
else
return GetFooForKind<KIND + 1>(); // try the next kind
}
// Specialization for LAST_KIND+1
template <> int GetFooForKind<LAST_KIND + 1>(POD *pod)
{
// kind didn't match anything in FIRST_KIND..LAST_KIND
throw UnknownKindException(kind, "GetFoo");
}
// Now POD function members can be implemented like this:
int POD::GetFoo()
{
return GetFooForKind<FIRST_KIND>(this);
}
Ответ 5
Вот пример использования любопытно повторяющегося шаблона шаблона. Это может удовлетворить ваши потребности, если вы знаете больше информации во время компиляции.
template<class DerivedType>
struct POD
{
int GetFoo()
{
return static_cast<DerivedType*>(this)->GetFoo();
}
int GetBar()
{
return static_cast<DerivedType*>(this).GetBar();
}
int GetBaz()
{
return static_cast<DerivedType*>(this).GetBaz();
}
int GetXyzzy()
{
return static_cast<DerivedType*>(this).GetXyzzy();
}
};
struct Derived1 : public POD<Derived1>
{
int GetFoo()
{
return 1;
}
//define all implementations
};
struct Derived2 : public POD<Derived2>
{
//define all implementations
};
int main()
{
Derived1 d1;
cout << d1.GetFoo() << endl;
POD<Derived1> *p = new Derived1;
cout << p->GetFoo() << endl;
return 0;
}
Ответ 6
Расширяя решение, с которым вы закончили, следующее разрешает отображение производных функций при инициализации программы:
#include <typeinfo>
#include <iostream>
#include <functional>
#include <vector>
enum Kind
{
Kind_First,
Kind_Derived1 = Kind_First,
Kind_Derived2,
Kind_Total
};
struct POD
{
size_t kind;
int GetFoo();
int GetBar();
};
struct VTable
{
std::function<int(POD*)> GetFoo;
std::function<int(POD*)> GetBar;
};
template<int KIND>
struct KindTraits
{
typedef POD KindType;
};
template<int KIND>
void InitRegistry(std::vector<VTable> &t)
{
typedef typename KindTraits<KIND>::KindType KindType;
size_t i = KIND;
t[i].GetFoo = [](POD *p) -> int {
return static_cast<KindType*>(p)->GetFoo();
};
t[i].GetBar = [](POD *p) -> int {
return static_cast<KindType*>(p)->GetBar();
};
InitRegistry<KIND+1>(t);
}
template<>
void InitRegistry<Kind_Total>(std::vector<VTable> &t)
{
}
struct Registry
{
std::vector<VTable> table;
Registry()
{
table.resize(Kind_Total);
InitRegistry<Kind_First>(table);
}
};
Registry reg;
int POD::GetFoo() { return reg.table[kind].GetFoo(this); }
int POD::GetBar() { return reg.table[kind].GetBar(this); }
struct Derived1 : POD
{
Derived1() { kind = Kind_Derived1; }
int GetFoo() { return 0; }
int GetBar() { return 1; }
};
template<> struct KindTraits<Kind_Derived1> { typedef Derived1 KindType; };
struct Derived2 : POD
{
Derived2() { kind = Kind_Derived2; }
int GetFoo() { return 2; }
int GetBar() { return 3; }
};
template<> struct KindTraits<Kind_Derived2> { typedef Derived2 KindType; };
int main()
{
Derived1 d1;
Derived2 d2;
POD *p;
p = static_cast<POD*>(&d1);
std::cout << p->GetFoo() << '\n';
p = static_cast<POD*>(&d2);
std::cout << p->GetBar() << '\n';
}