В С++ переопределение существующей виртуальной функции прерывается ABI?

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

Очевидно, что, поскольку стандарт С++ не предусматривает конкретного ABI, этот вопрос имеет определенную специфику для платформы, но на практике то, что разбивает и поддерживает ABI, похоже на большинство компиляторов. Меня интересует поведение GCC, но чем больше компиляторов люди могут ответить, тем полезнее этот вопрос будет:)

Ответ 1

Это может быть.

Вы ошибаетесь в отношении смещения. Смещение в таблице vtable уже определено. Что произойдет, так это то, что конструктор класса Derived заменит указатель на это смещение на Derived override (путем переключения v-указателя класса в новую v-таблицу). Таким образом, он, как правило, совместим с ABI.

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

Обычно, когда вы вызываете виртуальную функцию, компилятор вводит поиск в vtable через vpointer. Однако, если он может (статически) определить точный тип объекта, он также может вывести точную функцию для вызова и сбрить виртуальный поиск.

Пример:

struct Base {
  virtual void foo();
  virtual void bar();
};

struct Derived: Base {
  virtual void foo();
};

int main(int argc, char* argv[]) {
  Derived d;
  d.foo(); // It is necessarily Derived::foo
  d.bar(); // It is necessarily Base::bar
}

И в этом случае... просто связывание с вашей новой библиотекой не подберет Derived::bar.

Ответ 2

Это не похоже на то, на что в целом можно положиться вообще - как вы сказали, С++ ABI довольно сложно (даже до параметров компилятора).

Это говорит, что я думаю, что вы могли бы использовать g++ -fdump-class-hierarchy до и после того, как вы внесете это изменение, чтобы увидеть, изменились ли в родительском или дочернем vtables. Если они этого не делают, возможно, "справедливо" безопасно предположить, что вы не нарушили ABI.

Ответ 3

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

// V1
struct A { virtual void f(); };
struct B { virtual void g(); };
struct C : A, B { virtual void h(); }; //does not reimplement f or g;

// V2
struct C : A, B {
    virtual void h();
    virtual void g();  //added reimplementation of g()
};

Это изменяет компоновку C vtable, добавляя запись для g() (спасибо "Gof", чтобы привлечь внимание к ней в первую очередь, в качестве комментария в http://marcmutz.wordpress.com/2010/07/25/bcsc-gotcha-reimplementing-a-virtual-function/).

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

MyClass * c = new MyClass;
c->myVirtualFunction(); // not actually virtual at runtime

или создал его в стеке:

MyClass c;
c.myVirtualFunction(); // not actually virtual at runtime

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

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

Ответ 4

Моя интуиция заключается в том, что она должна менять смещение в vtbl, не изменяя при этом размер таблицы.

Ну, ваша интуиция явно ошибается:

  • либо есть новая запись в таблице vtable для переопределения, все следующие записи перемещаются, а таблица растет,
  • или нет новой записи, а представление vtable не изменяется.

Какое значение истинно, может зависеть от многих факторов.

В любом случае: не рассчитывает на это.

Ответ 5

Предостережение: см. В С++ переопределяет существующую виртуальную функцию break ABI? для случая, когда эта логика не выполняется;

В моих мыслях Марк, предлагающий использовать g++ -fdump-class-hierarchy, будет победителем здесь, сразу после правильных регрессионных тестов


Переопределяющие вещи не должны изменять компоновку vtable [1]. Сами записи vtable будут находиться в файле данных библиотеки IMHO, поэтому изменение в нем не должно создавать проблемы.

Конечно, приложения должны быть повторно подключены, в противном случае существует вероятность поломки, если потребитель использовал прямую ссылку на & Derived:: overriddenMethod; Я не уверен, разрешено ли компилятору разрешать это в Base:: overriddenMethod, но лучше безопасно, чем извините.

[1]: это предполагает, что метод был виртуальным для начала!