2010-10-14 1 views
2

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

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; 
}; 

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

+4

посмотреть YAGNI .... –

+1

Что заставляет вас думать, что то, за что вы не подготовлены, будет единым «пустотом»? –

+0

@Mitch Wheat: Я уверен, что это YAGNI просто, что это «ты ** ** Нужно ли это» :) – nakiya

ответ

2

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

API-интерфейс для Windows во многих местах поддерживает бинарную совместимость, поставив 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 С:

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 
    } 
}; 

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


Edit: Так как кажется, что это не так очевидно, вырезать, как я думал, что это, вот пример.
Предположим, что тот же самый 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.

+0

Означает ли это, что компилятор гарантирует, что переменные будут помещены в память в том же макете, что и код? – nakiya

+0

И я уверен, что эволюция линейна. – nakiya

+0

Спасибо за длинный ответ. Но я думаю, что вы, возможно, неправильно истолковали мои требования. Предположим, что в первой версии мы имеем заголовок 'H1.h'. Это будет включено в приложение 'A1' и общий объект' D1'. Заголовок будет объявлять структуру 'PluginInfo1' с базовой информацией. Во второй версии 'A' будут иметь заголовки' H1.h' и 'H2.h'. 'H2.h' объявит' PluginInfo2', который добавит другой член. 'A' будет загружать' D2', который использует 'PluginInfo2' из' H2.h'. И 'A' также придется загружать' D1'. Будет одна функция для извлечения «PluginInfo» из общего объекта. Что означает ... – nakiya

2

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

std::map<std::string, std::string> m_otherKeyValuePairs; хватило бы на ближайшие 500 лет.

Edit:

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

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

(отвратительная == не рекомендуется)

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

  • Преимущество:
    std::map член не подлежит object slicing. Когда старый исходный код копирует объект PluginInfo, который содержит новые ключи в сумке свойств, копируется весь пакет свойств.
  • Неудобство:
    многие программисты начинают добавлять несвязанные вещи в сумку собственности, и даже начинает писать код, который процессы значения, указанные в контейнере свойств, что приводит к обслуживанию кошмара.
+0

, конечно, любая сумка с общим имуществом имеет недостаток: она не сильно набрана ... (не мой нисходящий ответ) –

+0

@Mitch: Я отредактировал, чтобы было очевидно, что это не рекомендуется. Это так широко используется как [большой шар грязи] (http://www.infoq.com/news/2010/09/big-ball-of-mud). – rwong

2

Что предлагает rwong (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*>.

6

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

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

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

+0

Это хорошая идея (простая, которая работает и находится в духе C++). Следует соблюдать осторожность, чтобы исходный код пользователя всегда проходил * по ссылке *, чтобы избежать [проблемы отсечения объекта] (http://stackoverflow.com/questions/274626/what-is-the-slicing-problem-in -c), [также здесь] (http://stackoverflow.com/questions/274626#274636). – rwong

1

Вот идея, не уверен, работает ли он с классами, это наверняка работает с структурами: Вы можете сделать-структуру «резервный» некоторое пространство для использования в будущем, как это:

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

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

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

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

+0

Один недостаток, о котором я могу думать, - это знать 'sizeof()' для каждого используемого типа данных. Но все же это хорошо, я думаю. – nakiya

6

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

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

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

+0

+1. Спасибо за указание на использование базовых типов. – nakiya