Бинарная совместимость в C++

Binary Compatibility Issues With C++

Автор: Matthias Ettrich
Перевод: Andi Peredri

Определение

Библиотека является бинарно совместимой, если программа, собранная с предыдущей версией библиотеки, может работать с ее новой версией без перекомпиляции.

Если для обеспечения работы программы с новой версией библиотеки необходима ее перекомпиляция, но не требуются какие-либо изменения в исходном коде, библиотека является совместимой на уровне исходного кода.

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

В проекте KDE мы обеспечиваем бинарную совместимость на протяжении старшего номера версии.

Возможности и ограничения

Вы можете ...

Вы не можете ...

Технические приемы разработчиков библиотек

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

Битовые поля

Исключением являются битовые поля. Если вы в качестве компонентов структур и объединений используете битовые поля, то можете безопасно изменять их суммарный размер до ближайшего целого байта, минус 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 добавьте следующую декларацию:
class FooPrivate;

и d-указатель в закрытую секцию класса:
private:
	FooPrivate* d;

Сам класс FooPrivate целиком и полностью определяется в файле реализации класса ( обычно *.cpp ), например:
class FooPrivate {
public:
	FooPrivate()
	: m1(0), m2(0)
	{};
	int m1;
	int m2;
	QString s;
};

Все, что вам теперь осталось сделать, это создать в конструкторе или инициализирующей функции объект FooPrivate:
	d = new FooPrivate;

и затем удалить его в вашем деструкторе:
	delete d;

Естественно, вы не обязаны располагать абсолютно все данные-члены в классе 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.

Для использования этого приема вам необходимо проделать следующие шаги:

  1. Создайте закрытый объект класса FooPrivate.
  2. Создайте статический объект QPtrDict<FooPrivate>. Заметьте, что некоторые компиляторы/сборщики ( почти все, к сожалению ) не обеспечивают создание статических объектов в библиотеках. Они просто забывают вызвать конструктор. Поэтому вам необходимо использовать статический указатель на QPtrDict и следующую функцию для доступа к данным:
    	// BCI: Add a real d-pointer
    	static QPtrDict<FooPrivate>* d_ptr = 0;
    	static void cleanup_d_ptr()
    	{
    		delete d_ptr;
    	}
    	static FooPrivate* d( const Foo* foo )
    	{
    		if ( !d_ptr ) {
    			d_ptr = new QPtrDict<FooPrivate>
    			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 );
    	}
    
  3. Теперь вы можете использовать в вашем классе d-указатель почти так же просто, как и в предыдущем примере, используя вызов d(this). Например:
    	d(this)->m1 = 5;
    
  4. Добавьте следующую строку в ваш деструктор:
    	delete_d(this);
    
    Это не обязательно, но сэкономит часть ресурсов.
  5. Не забудьте добавить BCI-комментарий, чтобы этот "хак" был удален в следующих версиях библиотеки.
  6. Не забудьте добавить d-указатель в ваш следующий класс.