Есть ли способ подготовить структуру для будущих дополнений? - PullRequest
2 голосов
/ 14 октября 2010

У меня есть следующая структура, которая будет использоваться для хранения информации о плагине. Я очень уверен, что со временем это изменится (скорее всего). Что может быть лучше здесь, чем то, что я сделал, предполагая, что этот файл будет исправлен?

struct PluginInfo
{
    public:
        std::string s_Author;
        std::string s_Process;
        std::string s_ReleaseDate;
        //And so on...

        struct PluginVersion
        {
            public:
                std::string s_MajorVersion;
                std::string s_MinorVersion;
                //And so on...
        };
        PluginVersion o_Version;

        //For things we aren't prepared for yet.
        void* p_Future;
};

Далее, есть ли какие-то меры предосторожности, которые я должен принять при создании общих объектов для этой системы. Думаю, я столкнусь с множеством несовместимостей библиотек. Пожалуйста помоги. Спасибо

Ответы [ 6 ]

6 голосов
/ 14 октября 2010

Либо ваш плагин скомпилирован с той же версией компилятора C ++ и источника библиотеки std (либо его реализация std :: string может быть несовместима, и все ваши строковые поля будут повреждены), и в этом случае вам придется перекомпилировать плагины в любом случае, добавление полей в структуру не имеет значения

Или вы хотите двоичную совместимость с предыдущими плагинами, в этом случае придерживайтесь простых данных и массивов символов фиксированного размера (или предоставьте API для выделения памяти для строк на основе размера или передачи константного символа *), в котором на случай, если в структуре есть несколько неиспользуемых полей, а затем при необходимости меняйте их на элементы с полезными именами. В таких случаях также обычно в структуре есть поле, чтобы указать, какую версию он представляет.

Но очень редко можно ожидать двоичной совместимости и использовать std :: string. Вы никогда не сможете обновить или изменить свой компилятор.

6 голосов
/ 14 октября 2010

Как насчет этого, или я считаю слишком простым?

struct PluginInfo2: public PluginInfo
{
    public:
        std::string s_License;
};

В вашем приложении вы, вероятно, передаете только указатели на PluginInfo с, поэтому версия 2 совместима с версией 1. Когда вынужен доступ к членам версии 2, вы можете протестировать версию либо с dynamic_cast<PluginInfo2 *>, либо с явным членом pluginAPIVersion.

2 голосов
/ 14 октября 2010

Как сказал кто-то другой, для двоичной совместимости вы, скорее всего, ограничитесь API C.

Windows API во многих местах поддерживает двоичную совместимость, помещая член size в структуру:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;
};

Когда вы создаете такого зверя, вам нужно соответственно установить члена size:

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

Когда вы компилируете свой код с более новой версией API, где struct имеет дополнительные элементы, его размер изменяется, и это отмечается в его size элементе. Функции API при передаче такого struct предположительно сначала будут читать его член size и переходить по-разному для обработки struct, в зависимости от его размера.

Конечно, это предполагает, что эволюция является линейной, и новые данные всегда добавляются только в конце struct. То есть у вас никогда не будет разных версий такого типа, имеющих одинаковый размер.


Однако использование такого зверя - хороший способ гарантировать, что пользователь внесет ошибки в свой код. Когда они перекомпилируют свой код с использованием нового API, sizeof(pluginInfo) автоматически адаптируется, но дополнительные члены не устанавливаются автоматически. Разумная безопасность может быть достигнута путем «инициализации» пути struct C:

PluginInfo pluginInfo;
std::memset( &pluginInfo, 0, sizeof(pluginInfo) );
pluginInfo.size = sizeof(pluginInfo);

Однако, даже если оставить в стороне тот факт, что технически обнуление памяти может не дать разумного значения каждому члену (например, могут быть архитектуры, в которых все биты, установленные на ноль, не являются допустимым значением для типов с плавающей запятой), это раздражает и подвержено ошибкам, поскольку требует трехэтапного построения.

Выходом было бы создать небольшую и встроенную оболочку C ++ вокруг этого C API. Что-то вроде:

class CPPPluginInfo : PluginInfo {
public:
  CPPPluginInfo()
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
  }

  CPPPluginInfo(const char* author /* other data */)
   : PluginInfo() // initializes all values to 0
  {
    size = sizeof(PluginInfo);
    s_Author = author;
    // set other data
  }
};

Класс может даже позаботиться о сохранении строк, на которые указывают члены C struct, в буфере, так что пользователям класса даже не придется беспокоиться об этом.


Редактировать: Поскольку кажется, что это не так ясно, как я думал, вот пример.
Предположим, что тот же самый struct в более поздней версии API получит некоторого дополнительного члена:

struct PluginInfo
{
    std::size_t size; // should be sizeof(PluginInfo)

    const char* s_Author;
    const char* s_Process;
    const char* s_ReleaseDate;
    //And so on...

    struct PluginVersion
    {
        const char* s_MajorVersion;
        const char* s_MinorVersion;
        //And so on...
    };
    PluginVersion o_Version;

    int fancy_API_version2_member;
};

Когда плагин, связанный со старой версией API, теперь инициализирует struct следующим образом

PluginInfo pluginInfo;
pluginInfo.size = sizeof(pluginInfo);
// set other members

его struct будет старой версией, в которой отсутствует новый блестящий элемент данных из версии 2 API. Если теперь он вызывает функцию второго API, принимающего указатель на PluginInfo, он передаст адрес старого PluginInfo, одного короткого члена данных, в функцию нового API. Однако для функции API версии 2 pluginInfo->size будет меньше, чем sizeof(PluginInfo), поэтому он сможет это отловить и обработать указатель как указывающий на объект, который не имеет fancy_API_version2_member. (Предположительно, внутренняя часть API хост-приложения, PluginInfo - это новое и блестящее с fancy_API_version2_member, а PluginInfoVersion1 - это новое имя старого типа. Поэтому все, что нужно сделать новому API, это привести PluginInfo* это будет плагин в PluginInfoVersion1* и ответвление к коду, который может иметь дело с этой пыльной старой штукой.)

Иначе будет плагин, скомпилированный с новой версией API, где PluginInfo содержит fancy_API_version2_member, подключенный к более старой версии хост-приложения, которое ничего не знает об этом. Опять же, функции API хост-приложения могут поймать это, проверив, является ли pluginInfo->size больше , чем sizeof их собственный PluginInfo. Если так, то плагин предположительно был скомпилирован с более новой версией API, о чем знает приложение хоста. (Или при записи плагина не удалось правильно инициализировать элемент size. См. Ниже, как упростить работу с этой несколько хрупкой схемой.)
Есть два способа справиться с этим: Самый простой - просто отказаться от загрузки плагина. Или, если возможно, приложение хоста могло бы работать с этим так или иначе, просто игнорируя двоичный материал в конце объекта PluginInfo, который он передал, который он не знает, как интерпретировать.
Однако последнее сложно, так как вам нужно решить это при реализации старого API , не зная точно, как будет выглядеть новый API.

2 голосов
/ 14 октября 2010

То, что предлагает Руонг (std::map<std::string, std::string>), является хорошим направлением. Это позволяет добавлять преднамеренные строковые поля. Если вы хотите иметь больше гибкости, вы можете объявить абстрактный базовый класс

class AbstractPluginInfoElement { public: virtual std::string toString() = 0;};

и

class StringPluginInfoElement : public AbstractPluginInfoElement 
{ 
  std::string m_value;
  public: 
   StringPluginInfoElement (std::string value) { m_value = value; }
   virtual std::string toString() { return m_value;}
};

Затем вы можете получить более сложные классы, такие как PluginVersion и т. Д., И сохранить map<std::string, AbstractPluginInfoElement*>.

2 голосов
/ 14 октября 2010

Один отвратительный идея:

A std::map<std::string, std::string> m_otherKeyValuePairs; будет достаточно для следующих 500 лет.

Редактировать:

С другой стороны, это предложение настолько склонно к неправильному использованию, что может претендовать на TDWTF.

Другая столь же отвратительная идея :
a std::string m_everythingInAnXmlBlob;, как видно изреальное программное обеспечение.

(безобразный == не рекомендуется)

Редактировать 3:

  • Преимущество:
    Элемент std::map не подлежит объектному разрезанию .Когда старый исходный код копирует объект PluginInfo, который содержит новые ключи в пакете свойств, копируется весь пакет свойств.
  • Недостаток:
    многие программисты начнут добавлять несвязанные вещи в пакет свойств,и даже начинает писать код, который обрабатывает значения в наборе свойств, что приводит к кошмару обслуживания.
1 голос
/ 14 октября 2010

Вот идея, не уверенная, работает ли она с классами, она наверняка работает со структурами: Вы можете сделать структуру "резервной" для некоторого пространства, которое будет использоваться в будущем, следующим образом:

struct Foo
{
  // Instance variables here.
  int bar;

  char _reserved[128]; // Make the class 128 bytes bigger.
}

Инициализатор обнуляет всю структуру перед заполнением, поэтому более новые версии класса, которые будут обращаться к полям, которые теперь будут находиться в «зарезервированной» области, имеют нормальные значения по умолчанию.

Если вы только добавляете полей перед _reserved, соответственно уменьшая его размер, и не изменяя / переставляя другие поля, вы должны быть в порядке.Не нужно никакой магии.Старые программы не будут касаться новых полей, поскольку они не знают о них, и объем памяти останется прежним.

...