добавление виртуальной функции в конец объявления класса позволяет избежать двоичной несовместимости? - PullRequest
4 голосов
/ 18 мая 2010

Может ли кто-нибудь объяснить мне, почему добавление виртуальной функции в конец объявления класса позволяет избежать двоичной несовместимости?

Если у меня есть:

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
};

И позже измените объявление этого класса на:

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void someFuncC() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
};

Я получаю coredump из другого .so, скомпилированного с предыдущим объявлением. Но если я добавлю someFuncC () в конце объявления класса (после "int someVal"):

class A
{ 
  public:
    virtual ~A();
    virtual void someFuncA() = 0;
    virtual void someFuncB() = 0;
    virtual void other1() = 0;
  private:
    int someVal;
  public:
    virtual void someFuncC() = 0;
};

Я больше не вижу coredump. Может кто-нибудь сказать мне, почему это? И всегда ли работает этот трюк?

PS. компилятор gcc, это работает с другими компиляторами?

Ответы [ 5 ]

6 голосов
/ 18 мая 2010

С другой ответ :

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

Все это и бесконечное количество других возможностей заключены в один термин: Неопределенное поведение :

Просто держись от этого подальше.

Если вы знаете, как конкретная версия компилятора реализует свои функции, вы можете заставить это работать. Или нет. Или вы можете подумать, что это работает, но оно ломается, но только если тот, кто сидит перед компьютером, просто съел йогурт.

Есть ли какая-то причина, почему вы хотите, чтобы это сработало?

Я полагаю, что это работает / не работает так, как работает / не работает, потому что ваш компилятор создает записи виртуальной таблицы в порядке объявления виртуальных функций. Если вы испортили порядок, поместив виртуальную функцию между остальными, тогда, когда кто-то вызывает other1(), вместо этого вызывается someFuncC(), возможно, с неверными аргументами, но определенно в неправильный момент. (Радуйся, что он сразу падает.)
Это, однако, всего лишь предположение, и даже если оно верное, если ваша версия gcc не содержит документ, где-то описывающий это, нет гарантии, что он будет работать завтра даже с той же версией компилятора .

2 голосов
/ 18 мая 2010

Я немного удивлен, что эта конкретная перестановка помогает вообще. Это точно не гарантирует работу.

Класс, который вы даете выше, обычно переводится в следующий порядок:

typedef void (*vfunc)(void);

struct __A__impl { 
     vfunc __vtable_ptr;
     int someVal;
};

__A__impl__init(__A__impl *object) { 
    static vfunc virtual_functions[] = { __A__dtor, __A__someFuncA, __A__someFuncB};
    object->__vtable__ptr = virtual_functions;    
}

Когда / если вы добавите someFuncC, вы обычно должны добавить еще одну запись в таблицу виртуальных функций класса. Если компилятор организует это перед любой из других функций, вы столкнетесь с проблемой, когда попытка вызвать одну функцию фактически вызывает другую. Пока его адрес находится в конце таблицы виртуальных функций, вещи должны все еще работать. C ++ не гарантирует ничего о том, как устроены vtables (или даже что является vtable).

По отношению к обычным данным (нестатические) элементы должны располагаться в порядке возрастания, если отсутствует промежуточный спецификатор доступа (public:, protected: или private:).

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

1 голос
/ 18 мая 2010

Может быть, G ++ помещает свою "приватную" виртуальную таблицу в другое место, чем общедоступная.

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

0 голосов
/ 18 мая 2010

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

Редактировать: немного потерял смысл вашего вопроса. Windows предоставляет множество собственных способов обмена данными, например, GetProcAddress, __declspec (dllexport) и __declspec (dllimport). GCC должен предложить нечто подобное. Двоичная сериализация - это плохо.

Изменить еще раз: На самом деле, он не упомянул исполняемые файлы в своем посте. Совсем. Ни для чего он пытался использовать свою двоичную совместимость.

0 голосов
/ 18 мая 2010

Я предлагаю избегать всего, что связано с двоичной совместимостью или сериализацией с классом. Это открывает слишком много банок с червями и не хватает рыбы, чтобы съесть их всех.

При передаче данных предпочитайте XML или протокол на основе ASCII с определениями полей, а не двоичный протокол. Гибкий протокол поможет людям (включая вас) отлаживать и поддерживать протоколы. Протоколы ASCII и XML обеспечивают более высокую читаемость, чем двоичные.

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

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

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

...