Объект с множественным наследованием, совместно использующий один ресурс - ищет хороший шаблон проектирования

Надеюсь, что на этот вопрос ранее не было ответа, мне было очень трудно найти быстрое описание моей проблемы.

Я собираюсь написать 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';) в нескольких местах.

Добавление членов данных (или методов) на любом этапе должно быть сохранено (конечно, это увеличит размер).

Другие вопросы по тегам