Двоичная совместимость при использовании передачи по ссылке вместо передачи по указателю

Этот вопрос предназначен для продолжения этого вопроса: Каковы различия между переменной-указателем и ссылочной переменной в C ++?

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

В нашей (мультиплатформенной) платформе у нас есть требование быть двоично-совместимым между выпусками и отладочными сборками (и между различными выпусками платформы). В частности, двоичные файлы, которые мы собираем в режиме отладки, должны использоваться в сборках релизов и наоборот.
Чтобы достичь этого, мы используем только чистые абстрактные классы и POD в наших интерфейсах.

Рассмотрим следующий код:

class IMediaSerializable
{
public:
virtual tResult Serialize(int flags,
ISerializer* pSerializer,
IException** __exception_ptr) = 0;
//[…]
};

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

Чтобы сделать код намного понятнее и избавиться от некоторых ненужных проверок во время выполнения, мы бы хотели переписать этот код, используя передачу по ссылке.

class IMediaSerializable
{
public:
virtual tResult Serialize(int flags,
ISerializer& pSerializer,
IException*& __exception_ptr) = 0;
//[…]
};

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

ОБНОВИТЬ:
Чтобы прояснить ситуацию: этот вопрос не о бинарной совместимости между версией кода для передачи по указателю и версией для передачи по ссылке. Я знаю, что это не может быть двоичной совместимостью. Фактически у нас есть возможность изменить дизайн нашего API, для которого мы рассматриваем использование передачи по ссылке вместо передачи по указателю, не заботясь о двоичной совместимости (новый основной выпуск).
Вопрос только о бинарной совместимости, когда используется только версия кода, передаваемая по ссылке.

4

Решение

Совместимость двоичного ABI определяется тем, какой компилятор вы используете. Стандарт C ++ не охватывает проблему совместимости двоичного ABI.

Вам нужно будет проверить документацию вашего компилятора C ++, чтобы увидеть, что он говорит о бинарной совместимости.

6

Другие решения

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

Рассмотрим класс Foo, который экспортирует две функции:

class Foo
{
public:
void f(int*);
void f(int&);
};

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

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

Например, GCC присваивает этим именам следующие имена:

void Foo::f(int*) => _ZN3Foo1fEPi
void Foo::f(int&) => _ZN3Foo1fERi

уведомление P против R,

Поэтому, если вы измените сигнатуру функции, ваше приложение не сможет связать.

2

Обычно ссылки реализуются как скрытые указатели, поэтому обычно будет совместимость с ABI. Вы должны будете проверить документацию вашего конкретного компилятора и, возможно, реализацию, чтобы убедиться.

Однако в эпоху C ++ 11 ваши ограничения на чисто абстрактные классы и типы POD чрезмерно усердны.

C ++ 11 разбил концепцию pod на несколько частей. Стандартная компоновка охватывает большинство, если не все, «макет памяти» гарантий типа стручка.

Но у типов стандартной компоновки могут быть конструкторы и деструкторы (среди прочих различий).

Таким образом, вы можете сделать действительно дружественный интерфейс.

Вместо указателя интерфейса, управляемого вручную, напишите простой умный указатель.

template<class T>
struct value_ptr {
T* raw;
// ...
};

тот ->clone()s при копировании, перемещает указатель при перемещении, удаляет при уничтожении, и (поскольку он принадлежит вам) может быть гарантированно стабильным по сравнению с ревизиями библиотеки компилятора (в то время как unique_ptr не могу). Это в основном unique_ptr что поддерживает ->clone(), Также есть свой unique_ptr для значений, которые не могут быть дублированы.

Теперь вы можете заменить ваши чисто виртуальные интерфейсы парой типов. Во-первых, чистый виртуальный интерфейс (с T* clone() const обычно), а второй обычный тип:

struct my_regular_foo {
value_ptr< IFoo > ptr;
bool some_method() const { return ptr->some_method(); } // calls pure virtual method in IFoo
};

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

Эти типы живут в заголовочных файлах, которые предоставляет библиотека.

И расширение интерфейса IFoo Это хорошо. Просто добавьте новый метод к обоим IFoo в конце типа (который в большинстве ABI обратно совместим (!) — попробуйте), затем добавьте новый метод к my_regular_foo что вперед к нему. Как мы не изменили макет нашего my_regular_fooдаже если библиотека и клиентский код могут расходиться во мнениях относительно того, какие методы у нее есть, это нормально — все эти методы скомпилированы встроенными и никогда не экспортируются — и клиенты, которые знают, что используют более новую версию вашей библиотеки, могут использовать ее, и те, кто не знает, но используют это, хорошо (без восстановления).

Есть одна осторожная ошибка: если вы добавите перегрузку к IFoo метода (не переопределение: перегрузка) порядок виртуальных методов изменяется, и если вы добавляете новый virtual parent расположение виртуальной таблицы может измениться, и это работает надежно, только если все наследование ваших абстрактных классов virtual в вашем общедоступном API (с виртуальным наследованием, vtable имеет указатели на начало каждого vtable подклассов: так что каждый подкласс может иметь больший vtable без путаницы с адресом других функций виртуальных функций. добавьте в конец vtable код подкласса, используя более ранние заголовочные файлы, которые все еще могут найти более ранние методы).

Этот последний шаг — разрешение новых методов на ваших интерфейсах — может быть мостом далеко, так как вам придется исследовать гарантии ABI (на практике и нет) для макета vtable для каждого поддерживаемого компилятора.

2
По вопросам рекламы [email protected]