<!--StartFragment -->
Автор: Matthias Ettrich
Перевод: Andi Peredri
Если для обеспечения работы программы с новой версией библиотеки необходима
ее перекомпиляция, но не требуются какие-либо изменения в исходном коде,
библиотека является совместимой на уровне исходного кода.
Бинарная совместимость избавляет от множества хлопот. Она максимально
упрощает распространение программного обеспечения в пределах одной платформы.
Без гарантий бинарной совместимости разработчикам пришлось бы собирать свои
программы статически. Статически собранные программы являются плохим решением,
потому что они
- требовательны к ресурсам ( особенно к памяти )
- не обеспечивают устранение ошибок в библиотеках и обновление этих
библиотек.
В проекте KDE мы обеспечиваем бинарную совместимость на протяжении старшего
номера версии.
Возможности и ограничения
Вы можете ...
- добавлять новые невиртуальные функции.
- Переопределять виртуальные функции, определенные в одном из базовых
классов, если гарантировано, что программы, собранные с предыдущей
версией библиотеки, вызовут их реализацию из базового класса. Это
ненадежно и рискованно. Дважды подумайте, прежде чем так поступить.
- Изменять встраиваемые ( inline ) функции или делать встраиваемые функции
обычными, если гарантировано, что программы, собранные с предыдущей
версией библиотеки, вызовут их старую реализацию. Это ненадежно и
рискованно. Дважды подумайте, прежде чем так поступить. Вот почему
классы, для которых предполагается обеспечить бинарную совместимость, должны
всегда иметь невстраиваемый деструктор, даже если он пустой.
- Удалять закрытые ( private ) невиртуальные функции, если они не
вызываются какой-либо встраиваемой функцией.
- Изменять значение по умолчанию параметра функции. Однако использование
нового значения по умолчанию для аргумента возможно только после
перекомпиляции.
- Добавлять новые статические данные-члены класса.
- Добавлять новые классы.
Вы не можете ...
- добавлять новые виртуальные функции, так как это изменит таблицу
виртуальных функций и приведет к неработоспособности наследуемых классов. (
Однако в некоторых случаях это все же возможно - спрашивайте в группах
рассылки ).
- Изменять порядок виртуальных функций в объявлении класса. Это наверняка
приведет к измене содержимого таблицы виртуальных функций.
- Изменять сигнатуру функции. Заметьте, что расширение функции еще одним
параметром, даже если этот параметр имеет значение по умолчанию, приводит к
изменению сигнатуры функции. Поэтому такое решение не обеспечивает
бинарную совместимость ( только совместимость на уровне исходного кода ).
Просто добавьте другую функцию с таким же именем и расширенным списком
параметров и короткое примечание о решении бинарной совместимости ( Binary
Compatibiliy Issue, BCI ), чтобы в будущих версиях библиотеки эти две функции
были объединены в одну с аргументом по умолчанию. Заметьте, что изменение типа
возвращаемого значения не приводит к изменению сигнатуры функции ( как
минимум, в gcc ).
void functionname( int a );
void functionname( int a, int b ); //BCI: merge with int b = 0
|
- Изменять права доступа к методам и данным класса, например, с
private на public. Некоторые компиляторы включают эту
информацию в сигнатуру. Если вам необходимо сделать закрытую функцию
защищенной или даже открытой, то добавьте новую функцию, которая вызовет эту
закрытую. Примечание: в KDE распространено изменять права доступа на
методы в сторону большей доступности ( т.е private->protected->public ).
Нам известен только один компилятор, поступающий таким образом, это - MSVC++.
В любом случае, KDE не компилируется под Windows.
- Добавлять новые данные-члены в класс или менять их очередность в
объявлении ( не применимо к статическим данным ).
- Изменять иерархию существующих классов, не считая добавления новых.
Технические приемы разработчиков библиотек
Самой большой проблемой при написании библиотек является невозможность
безопасного добавления новых данных-членов, так как это приведет к изменению
размеров и расположения каждого класса, структуры и массива, содержащих объект
этого типа, включая наследуемые классы.
Битовые поля
Исключением являются битовые поля. Если вы в качестве компонентов структур и
объединений используете битовые поля, то можете безопасно изменять их суммарный
размер до ближайшего целого байта, минус 1. Класс с битовыми полями
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
|
может быть безопасно расширен до
uint m1 : 1;
uint m2 : 3;
uint m3 : 1;
uint m4 : 2; // new member
|
без потери бинарной совместимости. Максимальный
суммарный размер битовых полей не должен превышать 7 бит ( или 15, если их
начальный размер больше 8). Использование самого старшего бита на некоторых
компиляторах может привести к потере бинарной совместимости.
Использование d-указателей
Битовые поля и зарезервированные переменные являются хорошим, но не
достаточным решением. Настало время рассмотреть технику d-указателей. Термин
"d-указатель" ввел Arnt Gulbrandsen ( Trolltech ) для техники, использованной
при разработке библиотеки Qt, обеспечив ей одной из первых C++ GUI-библиотек
бинарную совместимость на протяжении многих выпусков. Эта техника была быстро
адаптирована как основной прием программирования многими разработчиками
KDE-библиотек. Это замечательное решение проблемы добавления новых закрытых
данных-членов в класс без потери бинарной совместимости.
Примечание: Техника d-указателей неоднократно описывалась в истории
информатики под различными именами, такими, как pimpl (pointer to
implementation), handle/body, чеширский кот. Он-лайн версии этих документов вы
можете найти с помощью Google, добавив "C++" в строку поиска.
В объявление вашего класса Foo добавьте следующую декларацию:
и d-указатель в закрытую секцию класса:
Сам класс FooPrivate целиком и полностью определяется в файле реализации
класса ( обычно *.cpp ), например:
class FooPrivate {
public:
FooPrivate()
: m1(0), m2(0)
{};
int m1;
int m2;
QString s;
};
|
Все, что вам теперь осталось сделать, это создать в конструкторе или
инициализирующей функции объект FooPrivate:
и затем удалить его в вашем деструкторе:
Естественно, вы не обязаны располагать абсолютно все данные-члены в классе
FooPrivate. Для повышения производительности часто используемые данные лучше
поместить непосредственно в класс Foo, тем более, что встраиваемые функции не
имеют доступа к данным FooPrivate. Заметьте также, что все данные, доступные
через d-указатель, являются закрытыми в пределах класса Foo. Чтобы сделать их
открытыми или защищенными, необходимо реализовать set- и get- функции, например:
QString Foo::string() const
{
return d->s;
}
void setString( const QString& s )
{
d->s = s;
}
|
Решение проблемы
Если у вас нет свободных битовых полей, зарезервированных переменных или
d-указателя, но вам непременно нужно добавить новую закрытую переменную, у вас
все еще остается возможность сделать это. Если ваш класс является производным от
QObject, вы можете поместить
дополнительные данные в специальный дочерний объект и затем найти его в списке
дочерних объектов. Список дочерних объектов может быть получен с помощью QObject::children().
Однако наиболее быстрым и предпочтительным способом хранения соответствий между
вашими объектами и дополнительными данными является использование хеш-таблиц.
Для этих целей Qt предлагает словарь на основе указателей QPtrDict.
Для использования этого приема вам необходимо проделать следующие шаги:
- Создайте закрытый объект класса FooPrivate.
- Создайте статический объект QPtrDict. Заметьте, что
некоторые компиляторы/сборщики ( почти все, к сожалению ) не обеспечивают
создание статических объектов в библиотеках. Они просто забывают вызвать
конструктор. Поэтому вам необходимо использовать статический указатель на
QPtrDict и следующую функцию для доступа к данным:
// BCI: Add a real d-pointer
static QPtrDict* d_ptr = 0;
static void cleanup_d_ptr()
{
delete d_ptr;
}
static FooPrivate* d( const Foo* foo )
{
if ( !d_ptr ) {
d_ptr = new QPtrDict
qAddPostRoutine( cleanup_d_ptr );
}
FooPrivate* ret = d_ptr->find( (void*) foo );
if ( ! ret ) {
ret = new FooPrivate;
d_ptr->replace( (void*) foo, ret );
}
return ret;
}
static void delete_d( const Foo* foo )
{
if ( d_ptr )
d_ptr->remove( (void*) foo );
}
|
- Теперь вы можете использовать в вашем классе d-указатель почти так же
просто, как и в предыдущем примере, используя вызов d(this). Например:
- Добавьте следующую строку в ваш деструктор:
Это не обязательно, но сэкономит часть
ресурсов.
- Не забудьте добавить BCI-комментарий, чтобы этот "хак" был удален в
следующих версиях библиотеки.
- Не забудьте добавить d-указатель в ваш следующий класс.
Источник qt.osdn.org.ua