Объект с множественным наследованием, совместно использующий один ресурс - ищет хороший шаблон проектирования
Надеюсь, что на этот вопрос ранее не было ответа, мне было очень трудно найти быстрое описание моей проблемы.
Я собираюсь написать C++ API, который должен компилироваться на микроконтроллере, а также на целевых ПК, который абстрагирует связь с некоторым аппаратным устройством. Режимы работы устройства, а также параметры управления могут изменяться во время выполнения, пока соединение остается неизменным. Соединение управляется отдельным классом, к которому мой экземпляр базового класса имеет защищенную ссылку. Базовое устройство выглядит так (упрощенный пример):
class DeviceBase
{
public:
void setOnOffState (bool onOff);
bool getOnOffState();
protected:
DeviceBase (Connection& c);
Connection& connection;
}
DeviceBase::DeciveBase (Connection& c) : connection (c) {};
void DeviceBase::setOnOffState (bool onOff) {connection.sendParameter (/* Parameter number for onOff */, onOff); };
bool DeviceBase::getOnPffState() {return connection.requestParameter (/* Parameter number for onOff */); };
Теперь есть несколько общих типов устройств, которые имеют общий набор параметров. Допустим, есть универсальный тип 1, который всегда имеет параметр A и параметр B, и универсальный тип 2, который всегда имеет параметр C и параметр D. Таким образом, их реализация может выглядеть так:
class GenericDeviceType1 : public DeviceBase
{
public:
void setParameterA (int parameterA);
int getParameterA();
void setParameterB (char parameterB);
char getParameterB();
protected:
GenericDeviceType1 (Connection& c);
}
GenericDeviceType1::GenericDeviceType1 (Connection& c) : DeviceBase (c) {};
void GenericDeviceType1::setParameterA (int parameterA) {connection.sendParameter (/* Parameter number for parameterA */, parameterA); };
int GenericDeviceType1::getParameterA() {return connection.requestParameter (/* Parameter number for parameterA */); };
//... and so on - I think you got the principle
Но это становится еще сложнее. Есть определенные ароматы каждого типа. Но некоторые разделяют некоторые группы параметров. Теперь я хотел бы создать их с множественным наследованием, например так:
class DeviceType1ParameterSetX // a device with parameters E and F
{
public:
void setParameterE (float parameterE);
float getParameterE();
void setParameterF (int parameterF);
int getParameterF();
}
class DeviceType1ParameterSetY // a device with parameters G and H
{
public:
void setParameterG (bool parameterG);
bool getParameterG();
void setParameterH (char parameterH);
char getParameterH();
}
class DeviceType1ParameterSetZ // a device with parameters I and J
{
public:
void setParameterI (int parameterI);
int getParameterI();
void setParameterJ (int parameterJ);
int getParameterJ();
}
class SpecificDeviceType11 : public GenericDeviceType1,
public DeviceType1ParameterSetX,
public DeviceType1ParameterSetZ
{
public:
SpecificDeviceType11 (Connection &c);
//...
}
class SpecificDeviceType12 : public GenericDeviceType1,
public DeviceType1ParameterSetX,
public DeviceType1ParameterSetY,
public DeviceType1ParameterSetZ
{
public:
SpecificDeviceType12 (Connection &c);
//...
}
Проблема с этим подходом: классы DeviceTypeNParameterSetM
ничего не знают о соединении, поэтому реализация их функций setter и getter, вызывающих экземпляр соединения, напрямую невозможна. Тем не менее, я действительно хотел бы не делать общедоступным элемент соединения базового класса, чтобы поддерживать API в чистоте. Я знаю, что могу хранить ссылку на соединение в каждом классе набора параметров, но мне кажется, что это пустая трата памяти в связи с тем фактом, что он может работать на микроконтроллере с небольшим объемом памяти и без возможности динамическое управление памятью. Поэтому в идеале объем памяти каждого конкретного экземпляра должен быть одинаковым.
Теперь мой вопрос: как может выглядеть решение, результатом которого является чистый публичный API? Я с нетерпением жду вдохновения! В качестве дополнительной информации: в итоге будет представлено около 150 различных видов устройств, поэтому я бы очень хотел, чтобы это было как можно более организованным и удобным для пользователя!
1 ответ
Обычный способ сделать это - создать базовый класс DeviceBase. public virtual
и включая его как public virtual
базовый класс всех различных классов ParameterSet, которые должны знать об этом. Тогда любой из них может получить доступ к соединению, если это необходимо.
Когда вы используете виртуальное наследование, как это, вам нужно явно инициализировать DeviceBase
Базовый класс в конструкторах каждого неабстрактного класса, но это не так уж сложно.
Моя первая попытка на самом деле неоптимальна (см. Историю редактирования, если вы заинтересованы в этом). Фактически невозможно выполнить множественное наследование и сохранить размер производного класса таким же, как и у "реального" базового класса, поскольку каждый родительский класс должен иметь отдельный адрес (даже если все, кроме одного родительского класса, пусты).
Вместо этого вы можете использовать хвостовое наследование следующим образом:
struct Connection {
template<class T>
void sendParameter(int,T); // implemented somewhere
template<class T>
T requestParameter(int); // implemented somewhere
};
class DeviceBase {
public:
void setOnOffState(bool onOff) { connection.sendParameter(0, onOff); }
bool getOnOffState() { return connection.requestParameter<bool>(0); }
protected:
DeviceBase(Connection& c) : connection(c) {}
template<class T>
void sendParameter(int i,T t) { connection.sendParameter(i,t); }
template<class T>
T requestParameter(int i) { return connection.requestParameter<T>(i); }
private:
Connection& connection;
};
template<class Base>
class DeviceType1ParameterSetX : public Base // a device with parameters A and B
{
public:
void setParameterA (float parameterA) { this->sendParameter(0xA, parameterA);}
float getParameterA() { return this->template requestParameter<float>(0xA);}
void setParameterB (int parameterB) { this->sendParameter(0xB, parameterB);}
int getParameterB() { return this->template requestParameter<int>(0xB);}
DeviceType1ParameterSetX(Connection& c) : Base(c) {}
};
template<class Base>
class DeviceType1ParameterSetY : public Base // a device with parameters C and D
{
public:
void setParameterC (float parameterC) { this->sendParameter(0xC, parameterC);}
float getParameterC() { return this->template requestParameter<float>(0xC);}
void setParameterD (int parameterD) { this->sendParameter(0xD, parameterD);}
int getParameterD() { return this->template requestParameter<int>(0xD);}
DeviceType1ParameterSetY(Connection& c) : Base(c) {}
};
template<class Base>
class DeviceType1ParameterSetZ : public Base // a device with parameters E and F
{
public:
void setParameterE (float parameterE) { this->sendParameter(0xE, parameterE);}
float getParameterE() { return this->template requestParameter<float>(0xE);}
void setParameterF (int parameterF) { this->sendParameter(0xF, parameterF);}
int getParameterF() { return this->template requestParameter<int>(0xF);}
DeviceType1ParameterSetZ(Connection& c) : Base(c) {}
};
class SpecificDeviceTypeXZ : public
DeviceType1ParameterSetX<
DeviceType1ParameterSetZ<
DeviceBase> >
{
public:
SpecificDeviceTypeXZ (Connection &c) : DeviceType1ParameterSetX(c) {}
//...
};
class SpecificDeviceTypeXY : public
DeviceType1ParameterSetX<
DeviceType1ParameterSetY<
DeviceBase> >
{
public:
SpecificDeviceTypeXY (Connection &c) : DeviceType1ParameterSetX(c) {}
//...
};
void foo(Connection& c)
{
SpecificDeviceTypeXY xy(c);
SpecificDeviceTypeXZ xz(c);
static_assert(sizeof(xy)==sizeof(void*), "xy must just contain a reference");
static_assert(sizeof(xz)==sizeof(void*), "xz must just contain a reference");
xy.setOnOffState(true);
xy.setParameterC(1.0f);
xz.setParameterF(xy.getParameterB());
}
Я несколько упростил ваш пример, чтобы сохранить некоторую печать (например, я пропустил GenericDeviceType1
который по существу был бы DeviceType1ParameterSetX<DeviceBase>
в моем примере), а имена / номера не соответствуют вашему примеру.
Чтобы поиграть здесь, это Godbolt-Link (подтверждение того, что размер не увеличивается): https://godbolt.org/z/BtNOe_ Здесь, rdi
будет содержать первый параметр указателя (который большую часть времени является неявным this
параметр), или указатель, подразумеваемый Connection& c
параметр foo
, esi
всегда будет содержать номер параметра i
(так как это первый целочисленный параметр для методов Connection
и, в зависимости от типа, следующий параметр (из sendParameter
звонки) будет проходить через xmm0
или же edx
, Возвращаемое значение будет в eax
для целых чисел и в xmm0
для чисел с плавающей запятой (все это при условии x86_64bit ABI).
Чтобы понять, что происходит, я также рекомендую вставить некоторые выходные данные отладки (например, cout << __PRETTY_FUNCTION__ << ' ' << this << '\n';
) в нескольких местах.
Добавление членов данных (или методов) на любом этапе должно быть сохранено (конечно, это увеличит размер).