具有多个继承共享一个资源的对象 - 寻找良好的设计模式

Object with multiple inheritance sharing one resource - looking for good design pattern

本文关键字:对象 寻找 设计模式 资源 继承 共享 一个      更新时间:2023-10-16

希望这以前没有被回答过,我发现很难找到对我的问题的快速描述。

我即将编写一个C++ API,该 API 应该在微控制器和 PC 目标上编译,该目标抽象了与某些硬件设备的通信。设备的操作模式以及要控制的参数可能会在运行时更改,而连接保持不变。连接由一个单独的类管理,我的基类实例对该类具有受保护的引用。基本设备如下所示(简化示例(:

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 种不同的特定设备风格,所以我真的很想让它尽可能有条理和用户友好!

执行此操作的常用方法是使 DeviceBase 基类public virtual,并将其作为需要了解它的所有各种 ParameterSet 类的public virtual基类包含在内。 然后,如果需要,他们中的任何一个都可以访问连接。

当你像这样使用虚拟继承时,你需要在每个非抽象类的构造函数中显式初始化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参数(,或由fooConnection& c参数隐含的指针。esi将始终i保存参数编号(因为它是Connection方法的第一个整数参数,并且根据类型,下一个参数(sendParameter调用(将通过xmm0edx传递。对于整数,返回值将以eax为单位,对于浮点数,返回值将以xmm0为单位(这都是假设x86_64bit ABI(。

为了了解会发生什么,我还建议在几个地方插入一些调试输出(如cout << __PRETTY_FUNCTION__ << ' ' << this << 'n';(。

在任何阶段添加数据成员(或方法(都应该保存(当然它会增加大小(。