Расширение класса и поддержание двоичной обратной совместимости - PullRequest
10 голосов
/ 16 декабря 2010

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

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

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

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

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

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

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

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

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

Спасибо.

Ответы [ 6 ]

3 голосов
/ 07 января 2011

Наконец, проверьте двоичную совместимость с помощью автоматизированных инструментов, таких как abi-Compliance Checker .

3 голосов
/ 16 декабря 2010

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

2 голосов
/ 07 января 2011

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

Причина, по которой вы не можете увеличить размер класса, состоит в том, что для кого-то, кто скомпилировал, используя старый размер, но использует новый расширенный класс, это означало бы, что член данных, сохраненный после вашего класса в их объекте (и больше, если вы добавите больше, чем 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 голос
/ 10 января 2011

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

РЕДАКТИРОВАТЬ : отчеты для библиотеки qt4 здесь .

0 голосов
/ 16 декабря 2010

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

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

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

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

...