2010-12-16 3 views
9

Я пытаюсь добавить новые функции в существующую библиотеку. Мне нужно будет добавить новые данные в иерархию классов, чтобы у корневого класса были доступ к ним. Любой человек должен иметь возможность получать эти данные, только подклассы могли бы установить его (т. Е. Публичный геттер и защищенный сеттер).Расширение класса и поддержка двоичной обратной совместимости

Для обеспечения обратной совместимости, я знаю, что я не должен делать любой из следующих (список включает только действия, имеющие отношение к моей проблеме):

  • Добавить или удалить виртуальные функции
  • Добавить или удалить переменные-члены
  • Изменение типа существующего переменной-члена
  • Изменить подпись существующей функции

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

В библиотеке широко используется pimpl идиома, но, к сожалению, класс корня, который я должен изменить, не используйте эту идиому. Однако подклассы используют эту идиому.

Теперь только решение, о котором я могу думать, моделирует переменную-член со статической хэш-картой. Поэтому я мог бы создать статическую хэш-карту, сохранить в ней этот новый элемент и реализовать для нее статические аксессоры. Нечто подобное (в псевдо C++):

class NewData {...}; 

class BaseClass 
{ 
protected: 
    static setNewData(BaseClass* instance, NewData* data) 
    { 
     m_mapNewData[instance] = data; 
    } 

    static NewData* getNewData(BaseClass* instance) 
    { 
     return m_mapNewData[instance]; 
    } 
private: 
    static HashMap<BaseClass*, NewData*> m_mapNewData;  
}; 

class DerivedClass : public BaseClass 
{ 
    void doSomething() 
    { 
     BaseClass::setNewData(this, new NewData()); 
    } 
}; 

class Outside 
{ 
    void doActions(BaseClass* action) 
    { 
     NewData* data = BaseClass::getNewData(action); 
     ... 
    } 
}; 

Теперь, в то время как это решение может работать, я считаю, это очень некрасиво (конечно, я мог бы также добавить нестатические функции доступа, но это не устранило бы уродства) ,

Есть ли другие решения?

Спасибо.

+0

Разве вы не добавляете `m_mapNewData` в класс root здесь, о котором вы сами говорили, это не так? – Jon 2010-12-16 10:22:30

+0

Вы уверены, что вам нужна * двойная * обратная совместимость? Я считаю это довольно странным в контексте класса C++, тем более для класса * base *. – 2010-12-16 10:26:08

+0

@Jon: Поскольку это * статическая переменная-член, я думаю, что она не нарушит двоичную обратную совместимость, так как (к моему недоумению) она не меняет размер или макет класса. @Martin: Да, мне нужна * двоичная * обратная совместимость. Это широко используемая библиотека с открытым исходным кодом, и мы хотим, чтобы все существующие приложения продолжали работать после изменений. – user544511 2010-12-16 10:44:45

ответ

3

Вы можете использовать decorator pattern. Декоратор может отображать новые элементы данных, и никаких изменений в существующих классах не потребуется. Это лучше всего работает, если клиенты получают свои объекты через фабрики, потому что тогда вы можете прозрачно добавлять декораторов.

-1

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

0

Трудно поддерживать двоичную совместимость - гораздо проще поддерживать совместимость только с интерфейсом.

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

  • Эти интерфейсы никогда не могут быть изменены в будущем, но вы можете добавить новые интерфейсы.
  • В этих интерфейсах вы можете использовать только примитивные типы, такие как указатели и заданные значения целых чисел или поплавков.У вас не должно быть интерфейсов, например, std :: strings или других не-примитивных типов.
  • При возврате указателей на данные, выделенные в DLL, вам необходимо предоставить виртуальный метод для освобождения, чтобы приложение удалило данные с помощью удаления DLL.
2

Наконец, проверьте совместимость двоичных файлов с помощью автоматических инструментов, таких как abi-compliance-checker.

2

Вы можете добавить экспортированные функции (import/export), не влияя на двоичную совместимость (гарантируя, что вы не удаляете какие-либо текущие функции и не добавляете новые функции в конце), но вы не можете увеличить размер класса, добавив новые данных.

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

например.

Старый:

class CounterEngine { 
public: 
    __declspec(dllexport) int getTotal(); 
private: 
    int iTotal; //4 bytes 
}; 

Новое:

class CounterEngine { 
    public: 
     __declspec(dllexport) int getTotal(); 
     __declspec(dllexport) int getMean(); 
    private: 
     int iTotal; //4 bytes 
     int iMean; //4 bytes 
    }; 

Клиент тогда может быть:

class ClientOfCounter { 
public: 
    ... 
private: 
    CounterEngine iCounter; 
    int iBlah; 
}; 

В памяти ClientOfCounter в старых рамках будет выглядеть примерно так:

ClientOfCounter: iCounter[offset 0], 
       iBlah[offset 4 bytes] 

тот же код (не перекомпилировать, но используя новую версию будет выглядеть)

ClientOfCounter: iCounter[offset 0], 
       iBlah[offset 4 bytes] 

т.е. он не знает, что iCounter теперь 8 байт, а не 4 байта, поэтому iBlah фактически разгромили последние 4 байта iCounter.

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

class CounterEngine { 
public: 
    __declspec(dllexport) int getTotal(); 
private: 
    int iTotal; //4 bytes 
    void* iSpare; //future 
}; 

class CounterEngineBody { 
    private: 
     int iMean; //4 bytes 
     void* iSpare[4]; //save space for future 
    }; 


    class CounterEngine { 
    public: 
     __declspec(dllexport) int getTotal(); 
     __declspec(dllexport) int getMean() { return iBody->iMean; } 
    private: 
     int iTotal; //4 bytes 
     CounterEngineBody* iBody; //now used to extend class with 'body' object 
    }; 
1

Если ваша библиотека с открытым исходным кодом, то вы можете запросить, чтобы добавить его к upstream-tracker. Он автоматически проверяет все выпуски библиотек для обратной совместимости. Таким образом, вы можете легко поддерживать свой API.

EDIT: отчеты для библиотеки qt4: here.

Смежные вопросы