В C ++ переопределение существующей виртуальной функции нарушает ABI? - PullRequest
11 голосов
/ 21 апреля 2011

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

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

Ответы [ 5 ]

9 голосов
/ 21 апреля 2011

Может.

Вы ошибаетесь в отношении смещения.Смещение в vtable уже определено.Что произойдет, так это то, что конструктор класса «Производные» заменит указатель функции с этим смещением на переопределение «Производные» (путем переключения 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.

7 голосов
/ 21 апреля 2011

Это не похоже на то, на что в целом можно было бы особенно положиться - как вы сказали, C ++ ABI довольно сложен (даже до параметров компилятора).* до и после внесения изменений, чтобы увидеть, изменится ли структура родительских или дочерних таблиц.Если они этого не делают, вероятно, «довольно» безопасно предположить, что вы не сломали ABI.

3 голосов
/ 21 апреля 2011

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

// 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()
};

Это меняет компоновку vtable таблицы C, добавляя запись для 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

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

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

1 голос
/ 08 августа 2012

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

Что ж, ваша интуиция явно ошибочна:

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

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

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

0 голосов
/ 21 апреля 2011

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

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


Переопределение не должно изменять макет vtable [1].Записи vtable сами по себе будут в сегменте данных библиотеки , ИМХО, поэтому изменение на it не должно создавать проблем.

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...