Я пытаюсь создать очень открытый плагин-фреймворк в c ++, и мне кажется, что я нашел способ сделать это, но ноющая мысль постоянно говорит мне, что что-то очень, очень неправильно в том, что я делаю и он либо не будет работать, либо вызовет проблемы.
Дизайн, который у меня есть для моей платформы, состоит из ядра, которое вызывает каждый плагин init
функция. Затем функция init оборачивается и использует ядро registerPlugin
а также registerFunction
чтобы получить уникальный идентификатор, а затем зарегистрировать каждую функцию, которую плагин хочет сделать доступным, используя этот идентификатор, соответственно.
Функция registerPlugin возвращает уникальный идентификатор. Функция registerFunction принимает этот идентификатор, имя функции и общий указатель на функцию, например:
bool registerFunction(int plugin_id, string function_name, plugin_function func){}
где plugin_function есть
typedef void (*plugin_function)();
Затем ядро берет указатель на функцию и помещает его в карту с function_name
а также plugin_id
, Все плагины, регистрирующие свою функцию, должны преобразовать функцию в тип plugin_function
,
Чтобы получить функцию, другой плагин вызывает ядро
plugin_function getFunction(string plugin_name, string function_name);
Тогда этот плагин должен разыграть plugin_function
к своему первоначальному типу, чтобы его можно было использовать. Он знает (теоретически), что такое правильный тип, имея доступ к .h
файл с описанием всех функций, которые плагин делает доступными. Кстати, плагины реализованы в виде динамических библиотек.
Это умный способ решить задачу, позволяющую различным плагинам соединяться друг с другом? Или это сумасшедшая и действительно ужасная техника программирования? Если это так, пожалуйста, укажите мне правильный путь для достижения этой цели.
РЕДАКТИРОВАТЬ: Если требуется какое-либо разъяснение, спросите, и оно будет предоставлено.
Указатели на функции — странные существа. Они не обязательно имеют тот же размер, что и указатели данных, и, следовательно, их нельзя безопасно привести к void*
и назад. Но спецификации C ++ (и C) позволяют любой указатель на функцию, который будет безопасно приведен к другому указателю на функцию тип (хотя вам придется затем привести его обратно к более раннему типу, прежде чем вызывать его, если вы хотите определенного поведения). Это похоже на возможность безопасно привести любой указатель данных к void*
и назад.
Указатели на методы становятся там, где он становится действительно волосатым: указатель на метод может быть больше обычного указателя на функцию, в зависимости от компилятора, 32-разрядного или 64-разрядного приложения и т. Д. Но еще интереснее то, что даже на один и тот же компилятор / платформа, не все указатели на методы имеют одинаковый размер: указатели на виртуальные функции могут быть больше, чем обычные указатели на методы; если задействовано множественное наследование (например, виртуальное наследование в ромбовидной структуре), указатели метода могут быть еще больше. Это зависит от компилятора и платформы. Это также причина того, что трудно создавать функциональные объекты (которые обертывают произвольные методы, а также свободные функции), особенно без выделения памяти в куче (это просто возможно использование шаблон колдовства).
Поэтому, используя указатели функций в вашем интерфейсе, для авторов плагинов становится непрактичным передавать указатели методов в вашу среду, даже если они используют один и тот же компилятор. Это может быть приемлемым ограничением; Подробнее об этом позже.
Поскольку нет гарантии, что указатели на функции будут одинакового размера от одного компилятора к другому, регистрируя указатели функций, вы ограничиваете авторов плагинов компиляторами, которые реализуют указатели функций того же размера, что и ваш компилятор. Это не обязательно будет так плохо на практике, поскольку размеры указателей на функции имеют тенденцию быть стабильными в разных версиях компилятора (и даже могут быть одинаковыми для нескольких компиляторов).
Реальные проблемы начинают возникать, когда вы хотите вызвать функции, на которые указывают указатели функций; Вы не можете безопасно вызывать функцию вообще, если не знаете ее истинную подпись (вы будут получить плохие результаты, начиная от «не работает» до ошибок сегментации). Таким образом, авторы плагина будут ограничены только регистрацией void
функции, которые не принимают параметров.
Хуже того: то, как на самом деле работает вызов функции на уровне ассемблера, зависит не только от размера сигнатуры и указателя на функцию. Существует также соглашение о вызовах, способ обработки исключений (стек должен быть правильно размотан при возникновении исключения) и фактическая интерпретация байтов указателя функции (если он больше, чем указатель данных, что делают дополнительные байты означать? В каком порядке?). На этом этапе автор плагина в значительной степени ограничен использованием того же компилятора (и версии!), Что и вы, и должен быть осторожен, чтобы соответствовать соглашению о вызовах и опциям обработки исключений (с компилятором MSVC ++, например, обработкой исключений). только явно включен с /EHsc
), а также используйте только обычные указатели на функции с точной сигнатурой, которую вы определили.
Все ограничения пока Можно считаться разумным, если немного ограничивающим. Но мы еще не закончили.
Если вы добавите std::string
(или почти любой части STL), все становится еще хуже, потому что даже с одним и тем же компилятором (и версией), есть несколько различных флагов / макросов, которые управляют STL; Эти флаги могут влиять на размер и значение байтов, представляющих строковые объекты. По сути, это похоже на два разные структурировать объявления в отдельных файлах с одинаковыми именами и надеяться, что они будут взаимозаменяемыми; очевидно, это не работает. Пример флага _HAS_ITERATOR_DEBUGGING
, Обратите внимание, что эти параметры могут даже меняться между режимами отладки и выпуска! Эти типы ошибок не всегда проявляются сразу / последовательно, и их очень трудно отследить.
Вы также должны быть очень осторожны с динамическим управлением памятью между модулями, так как new
в одном проекте может быть определено иначе new
в другом проекте (например, он может быть перегружен). При удалении у вас может быть указатель на интерфейс с виртуальным деструктором, то есть vtable
необходимо правильно delete
объект, и различные компиляторы все реализуют vtable
по-другому. В общем, вы хотите, чтобы модуль, который выделяет объект, был тем, который освобождает его; более конкретно, вы хотите код это освобождает объект для компиляции в тех же условиях, что и код, который его выделил. Это одна из причин std::shared_ptr
может принимать аргумент «delete» при его создании — потому что даже с тем же компилятором и флагами (единственный гарантированный безопасный способ поделиться shared_ptr
между модулями), new
а также delete
не может быть одинаковым везде shared_ptr
может быть уничтожен. С помощью средства удаления код, который создает общий указатель, также управляет его уничтожением. (Я просто добавил этот абзац для хорошей меры; кажется, вы не разделяете объекты через границы модуля.)
Все это является следствием того, что C ++ не имеет стандартного двоичного интерфейса (ABI); Это все для всех, где очень легко выстрелить себе в ногу (иногда даже не осознавая этого).
Так есть ли надежда? Еще бы! Вместо этого вы можете представить C API своим плагинам, и ваши плагины также предоставят C API. Это очень хорошо, потому что C API может взаимодействовать практически с любым языком. Вам не нужно беспокоиться об исключениях, за исключением того, что они не всплывают над функциями плагина (это забота авторов), и они стабильны независимо от компилятора / опций (при условии, что вы не пропустите контейнеры STL). и тому подобное). Там только одно стандартное соглашение о вызовах (cdecl
), который является значением по умолчанию для объявленных функций extern "C"
, void*
на практике они будут одинаковыми для всех компиляторов на одной и той же платформе (например, 8 байтов на x64).
Вы (и авторы плагинов) по-прежнему можете писать свой код на C ++, при условии, что вся внешняя связь между ними использует C API (т.е. притворяется модулем C для взаимодействия).
Указатели на функции C, вероятно, также совместимы между компиляторами на практике, хотя, если вы не хотите зависеть от этого, вы можете попросить плагин зарегистрировать функцию название (const char*
) вместо адреса, и затем вы можете извлечь адрес самостоятельно, например, LoadLibrary
с GetProcAddress
для Windows (аналогично, в Linux и Mac OS X есть dlopen
а также dlsym
). Это работает, потому что имя-коверкая отключено для функций, объявленных с extern "C"
,
Обратите внимание, что нет прямого способа ограничить зарегистрированные функции одним типом прототипа (в противном случае, как я уже сказал, вы не можете вызывать их должным образом). Если вам нужно передать конкретный параметр функции плагина (или вернуть значение), вам нужно зарегистрировать и вызывать разные функции с разными прототипами отдельно (хотя вы можете свернуть все указатели функций до общего указателя на функцию). введите внутренне, и только отбрасывайте в последнюю минуту).
Наконец, хотя вы не можете напрямую поддерживать указатели на методы (которые даже не существуют в C API, но имеют переменный размер даже при использовании C ++ API и поэтому не могут быть легко сохранены), вы можете разрешить плагинам предоставлять «пользовательский data «непрозрачный указатель при регистрации их функции, который передается функции всякий раз, когда она вызывается; это дает авторам плагинов простой способ написать обертки функций вокруг методов и сохранить объект для применения метода в параметре пользовательских данных. Параметр user-data также может использоваться для всего, что хочет автор плагина, что значительно упрощает взаимодействие и расширение вашей системы плагинов. Другим примером использования является адаптация между различными прототипами функций с использованием оболочки и дополнительных аргументов, хранящихся в пользовательских данных.
Эти предложения приводят к тому, что код выглядит примерно так (для Windows — код очень похож на другие платформы):
// Shared header
extern "C" {
typedef void (*plugin_function)(void*);
bool registerFunction(int plugin_id, const char* function_name, void* user_data);
}
// Your plugin registration code
hModule = LoadLibrary(pluginDLLPath);
// Your plugin function registration code
auto pluginFunc = (plugin_function)GetProcAddress(hModule, function_name);
// Store pluginFunc and user_data in a map keyed to function_name
// Calling a plugin function
pluginFunc(user_data);
// Declaring a plugin function
extern "C" void aPluginFunction(void*);
class Foo { void doSomething() { } };
// Defining a plugin function
void aPluginFunction(void* user_data)
{
static_cast<Foo*>(user_data)->doSomething();
}
Извините за длину этого ответа; большинство из них можно суммировать с помощью «стандарта C ++ не распространяется на взаимодействие; вместо этого используйте C, поскольку он по крайней мере имеет де-факто стандарты «.
Примечание. Иногда проще всего спроектировать нормальный API C ++ (с указателями функций или интерфейсами или любым другим способом) в предположении, что плагины будут скомпилированы при точно таких же обстоятельствах; Это разумно, если вы ожидаете, что все плагины будут разработаны вами (т.е. библиотеки DLL являются частью ядра проекта). Это также может сработать, если ваш проект с открытым исходным кодом, и в этом случае каждый может самостоятельно выбрать связную среду, в которой компилируются проект и плагины, но тогда это затрудняет распространение плагинов, за исключением исходного кода.
Обновить: Как отмечено в комментариях ern0, можно абстрагировать детали взаимодействия модуля (через C API), чтобы и основной проект, и плагины работали с более простым C ++ API. Далее следует краткое описание такой реализации:
// iplugin.h -- shared between the project and all the plugins
class IPlugin {
public:
virtual void register() { }
virtual void initialize() = 0;
// Your application-specific functionality here:
virtual void onCheeseburgerEatenEvent() { }
};
// C API:
extern "C" {
// Returns the number of plugins in this module
int getPluginCount();
// Called to register the nth plugin of this module.
// A user-data pointer is expected in return (may be null).
void* registerPlugin(int pluginIndex);
// Called to initialize the nth plugin of this module
void initializePlugin(int pluginIndex, void* userData);
void onCheeseBurgerEatenEvent(int pluginIndex, void* userData);
}// pluginimplementation.h -- plugin authors inherit from this abstract base class
#include "iplugin.h"class PluginImplementation {
public:
PluginImplementation();
};// pluginimplementation.cpp -- implements C API of plugin too
#include <vector>
struct LocalPluginRegistry {
static std::vector<PluginImplementation*> plugins;
};
PluginImplementation::PluginImplementation() {
LocalPluginRegistry::plugins.push_back(this);
}
extern "C" {
int getPluginCount() {
return static_cast<int>(LocalPluginRegistry::plugins.size());
}
void* registerPlugin(int pluginIndex) {
auto plugin = LocalPluginRegistry::plugins[pluginIndex];
plugin->register();
return (void*)plugin;
}
void initializePlugin(int pluginIndex, void* userData) {
auto plugin = static_cast<PluginImplementation*>(userData);
plugin->initialize();
}
void onCheeseBurgerEatenEvent(int pluginIndex, void* userData) {
auto plugin = static_cast<PluginImplementation*>(userData);
plugin->onCheeseBurgerEatenEvent();
}
}// To declare a plugin in the DLL, just make a static instance:
class SomePlugin : public PluginImplementation {
virtual void initialize() { }
};
SomePlugin plugin; // Will be created when the DLL is first loaded by a process// plugin.h -- part of the main project source only
#include "iplugin.h"#include <string>
#include <vector>
#include <windows.h>
class PluginRegistry;
class Plugin : public IPlugin {
public:
Plugin(PluginRegistry* registry, int index, int moduleIndex)
: registry(registry), index(index), moduleIndex(moduleIndex)
{
}
virtual void register();
virtual void initialize();
virtual void onCheeseBurgerEatenEvent();
private:
PluginRegistry* registry;
int index;
int moduleIndex;
void* userData;
};
class PluginRegistry {
public:
registerPluginsInModule(std::string const& modulePath);
~PluginRegistry();
public:
std::vector<Plugin*> plugins;
private:
extern "C" {
typedef int (*getPluginCountFunc)();
typedef void* (*registerPluginFunc)(int);
typedef void (*initializePluginFunc)(int, void*);
typedef void (*onCheeseBurgerEatenEventFunc)(int, void*);
}
struct Module {
getPluginCountFunc getPluginCount;
registerPluginFunc registerPlugin;
initializePluginFunc initializePlugin;
onCheeseBurgerEatenEventFunc onCheeseBurgerEatenEvent;
HMODULE handle;
};
friend class Plugin;
std::vector<Module> registeredModules;
}// plugin.cpp
void Plugin::register() {
auto func = registry->registeredModules[moduleIndex].registerPlugin;
userData = func(index);
}
void Plugin::initialize() {
auto func = registry->registeredModules[moduleIndex].initializePlugin;
func(index, userData);
}
void Plugin::onCheeseBurgerEatenEvent() {
auto func = registry->registeredModules[moduleIndex].onCheeseBurgerEatenEvent;
func(index, userData);
}
PluginRegistry::registerPluginsInModule(std::string const& modulePath) {
// For Windows:
HMODULE handle = LoadLibrary(modulePath.c_str());
Module module;
module.handle = handle;
module.getPluginCount = (getPluginCountFunc)GetProcAddr(handle, "getPluginCount");
module.registerPlugin = (registerPluginFunc)GetProcAddr(handle, "registerPlugin");
module.initializePlugin = (initializePluginFunc)GetProcAddr(handle, "initializePlugin");
module.onCheeseBurgerEatenEvent = (onCheeseBurgerEatenEventFunc)GetProcAddr(handle, "onCheeseBurgerEatenEvent");
int moduleIndex = registeredModules.size();
registeredModules.push_back(module);
int pluginCount = module.getPluginCount();
for (int i = 0; i < pluginCount; ++i) {
auto plugin = new Plugin(this, i, moduleIndex);
plugins.push_back(plugin);
}
}
PluginRegistry::~PluginRegistry() {
for (auto it = plugins.begin(); it != plugins.end(); ++it) {
delete *it;
}
for (auto it = registeredModules.begin(); it != registeredModules.end(); ++it) {
FreeLibrary(it->handle);
}
}// When discovering plugins (e.g. by loading all DLLs in a "plugins" folder):
PluginRegistry registry;
registry.registerPluginsInModule("plugins/cheeseburgerwatcher.dll");
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
(*it)->register();
}
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
(*it)->initialize();
}
// And then, when a cheeseburger is actually eaten:
for (auto it = registry.plugins.begin(); it != registry.plugins.end(); ++it) {
auto plugin = *it;
plugin->onCheeseBurgerEatenEvent();
}
Это дает преимущество использования C API для совместимости, но также предлагает более высокий уровень абстракции для плагинов, написанных на C ++ (и для основного кода проекта, который является C ++). Обратите внимание, что он позволяет определять несколько плагинов в одной DLL. Вы также можете устранить некоторые дубликаты имен функций, используя макросы, но я решил не делать этого простого примера.
Кстати, все это предполагает, что плагины не имеют взаимозависимостей — если плагин A влияет (или требуется) на плагин B, вам необходимо разработать безопасный метод для внедрения / построения зависимостей по мере необходимости, поскольку нет способа гарантировать в каком порядке плагины будут загружены (или инициализированы). Двухэтапный процесс будет хорошо работать в этом случае: загрузить и зарегистрировать все плагины; при регистрации каждого плагина, пусть они регистрируют любые услуги, которые они предоставляют. Во время инициализации создайте требуемые сервисы по мере необходимости, просматривая зарегистрированную таблицу сервисов. Это гарантирует, что все сервисы, предлагаемые всеми плагинами, зарегистрированы до любой из них пытается использоваться независимо от того, в каком порядке плагины регистрируются или инициализируются.
Подход, который вы выбрали, в целом нормален, но я вижу несколько возможных улучшений.
Ваше ядро должно экспортировать функции C с обычным соглашением о вызовах (cdecl или, возможно, stdcall, если вы работаете в Windows) для регистрации плагинов и функций. Если вы используете функцию C ++, то вы заставляете всех авторов плагинов использовать ту же версию компилятора и компилятора, что и вы, поскольку многие вещи, такие как искажение имени функции C ++, реализация STL и соглашения о вызовах, зависят от компилятора.
Плагины должны экспортировать только функции C, такие как ядро.
Из определения getFunction
Кажется, у каждого плагина есть имя, которое другие плагины могут использовать для получения своих функций. Это небезопасная практика, два разработчика могут создавать два разных плагина с одинаковым именем, поэтому, когда плагин запрашивает какой-то другой плагин по имени, он может получить плагин, отличный от ожидаемого. Лучшим решением было бы, чтобы плагины были публичными GUID. Этот GUID может появиться в заголовочном файле каждого плагина, так что другие плагины могут ссылаться на него.
Вы не реализовали управление версиями. В идеале вы хотите, чтобы ваше ядро было версионным, потому что вы обязательно измените его в будущем. Когда плагин регистрируется в ядре, он передает версию API ядра, с которой он был скомпилирован. Затем ядро может решить, можно ли загрузить плагин. Например, если ядро версии 1 получает запрос на регистрацию для плагина, для которого требуется ядро версии 2, у вас есть проблема, лучший способ решить эту проблему — не допустить загрузки плагина, поскольку ему могут потребоваться функции ядра, которых нет в старая версия Обратный случай также возможен, ядро v2 может загружать или не загружать плагины, созданные для ядра v1, и, если это разрешает, может потребоваться адаптация к более старому API.
Я не уверен, что мне нравится идея, что плагин может найти другой плагин и напрямую вызывать его функции, так как это нарушает инкапсуляцию. Мне кажется, лучше, если плагины объявляют о своих возможностях ядру, чтобы другие плагины могли найти нужные им сервисы по возможности, а не по адресу других плагинов по имени или GUID.
Имейте в виду, что любой плагин, который выделяет память, должен предоставлять функцию освобождения для этой памяти. Каждый плагин может использовать разные библиотеки времени выполнения, поэтому память, выделенная плагином, может быть неизвестна другим плагинам или ядру. Распределение и освобождение в одном модуле позволяет избежать проблем.
C ++ не имеет ABI. То, что вы хотите делать, имеет ограничение: плагины и ваш фреймворк должны компилироваться & ссылка того же компилятора & компоновщик с тем же параметром в той же ОС. Это бессмысленно, если достижение заключается во взаимодействии в форме бинарного распределения, потому что каждый плагин, разработанный для фреймворка, должен подготовить много версий, предназначенных для разных компиляторов на разных ОС. Поэтому исходный код distrbute будет более практичным, чем этот, и это путь GNU (скачайте src, настройте и сделайте)
COM это выбор, но он слишком сложный и устаревший. Или управляемый C ++ на .Net runtime. Но они только на MS OS. Если вы хотите универсальное решение, я предлагаю вам перейти на другой язык.
Как упоминает Джин, так как нет стандартного C ++ ABI и стандартных соглашений о распределении имен, вы застряли, чтобы компилировать вещи с помощью одного и того же компилятора и компоновщика. Если вам нужны плагины с разделяемой библиотекой / dll, вы должны использовать что-то вроде C-ish.
Если все будет скомпилировано с одним и тем же компилятором и компоновщиком, вы можете также рассмотреть std :: function.
typedef std::function<void ()> plugin_function;
std::map<std::string, plugin_function> fncMap;
void register_func(std::string name, plugin_function fnc)
{
fncMap[name] = fnc;
}
void call(std::string name)
{
auto it = fncMap.find(name);
if (it != fncMap.end())
(it->second)(); // it->second is a function object
}///////////////
void func()
{
std::cout << "plain" << std::endl;
}
class T
{
public:
void method()
{
std::cout << "method" << std::endl;
}
void method2(int i)
{
std::cout << "method2 : " << i << std::endl;
}
};T t; // of course "t" needs to outlive the map, you could just as well use shared_ptr
register_func("plain", func);
register_func("method", std::bind(&T::method, &t));
register_func("method2_5", std::bind(&T::method2, &t, 5));
register_func("method2_15", std::bind(&T::method2, &t, 15));
call("plain");
call("method");
call("method2_5");
call("method2_15");
Вы также можете иметь функции плагинов, которые принимают аргументы. Это будет использовать заполнители для std :: bind, но вскоре вы обнаружите, что ему не хватает boost :: bind. Boost Bind имеет хорошую документацию и примеры.
Нет никаких причин, почему вы не должны делать это. В C ++ использование этого стиля указателя является лучшим, поскольку это просто простой указатель. Я не знаю ни одного популярного компилятора, который бы делал что-то столь же мертвое, как создание указателя на функцию, как обычный указатель. За пределами разума кто-то может сделать что-то ужасное.
Стандарт плагина Vst работает аналогичным образом. Он просто использует указатели на функции в .dll и не имеет способов прямого вызова классов. Vst является очень популярным стандартом, и в Windows люди используют практически любой компилятор для создания плагинов Vst, включая Delphi, основанный на паскалях и не имеющий ничего общего с C ++.
Так что я бы сделал именно то, что вы предлагаете лично. Для общеизвестных плагинов я бы не использовал строковое имя, а целочисленный индекс, который можно найти гораздо быстрее.
Альтернативой является использование интерфейсов, но я не вижу причин, если ваше мышление уже основано на указателях функций.
Если вы используете интерфейсы, то вызывать функции из других языков не так просто. Вы можете сделать это из Delphi, но как насчет .NET.
С вашим предложением стиля указателя функции вы можете использовать .NET, например, для создания одного из плагинов. Очевидно, вам нужно будет разместить Mono в своей программе, чтобы загрузить его, но только для гипотетических целей это иллюстрирует простоту этого.
Кроме того, когда вы используете интерфейсы, вы должны начать подсчитывать ссылки, что неприятно. Вставьте свою логику в указатели на функции, как вы предлагаете, а затем оберните элемент управления в некоторых классах C ++, чтобы выполнить вызовы и прочее для вас. Затем другие люди могут создавать плагины с другими языками, такими как Delphi Pascal, Free Pascal, C, другие компиляторы C ++ и т. Д.
Но, как всегда, независимо от того, что вы делаете, обработка исключений между компиляторами останется проблемой, поэтому вам нужно подумать об обработке ошибок. Лучший способ заключается в том, что собственный метод плагинов перехватывает собственные исключения плагинов и возвращает код ошибки ядру и т. Д.
Со всеми превосходными ответами выше, я просто добавлю, что эта практика на самом деле довольно широко распространена. В своей практике я видел это как в коммерческих проектах, так и в свободно распространяемых / открытых источниках.
Так что — да, это хорошая и проверенная архитектура.
Вам не нужно регистрировать функции вручную. В самом деле? В самом деле.
То, что вы можете использовать, — это прокси-реализация для вашего интерфейса плагина, где каждая функция прозрачно загружает свой оригинал из общей библиотеки по требованию и вызывает его. Кто бы ни достиг прокси-объекта этого определения интерфейса, он может просто вызывать функции. Они будут загружены по требованию.
Если плагины являются синглетонами, тогда вообще не требуется ручная привязка (в противном случае необходимо выбрать правильный экземпляр).
Идея для разработчика нового плагина состояла бы в том, чтобы сначала описать интерфейс, затем создать генератор, который генерирует заглушку для реализации совместно используемой библиотеки, и дополнительно прокси-класс плагина с той же сигнатурой, но с автозагрузкой по требованию, которая затем используется в клиентском программном обеспечении. Оба должны выполнять один и тот же интерфейс (в C ++ чистый абстрактный класс).