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

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

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


person Joseph Garvin    schedule 21.04.2011    source источник
comment
Если вы делаете ABI, почему бы вам не использовать простой C для интерфейсов?   -  person sashoalm    schedule 21.04.2011
comment
Потому что это библиотека C++, предназначенная для идиоматического использования C++. Как вы думаете, скажем, QT должен предлагать свой API только на C? :П   -  person Joseph Garvin    schedule 21.04.2011


Ответы (5)


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

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

person Matthieu M.    schedule 21.04.2011
comment
Вот почему виртуальные методы всегда должны быть закрытыми. - person p12; 19.02.2012
comment
@ grumm143: Однако этого недостаточно. Встраивание все еще может сработать. - person Matthieu M.; 19.02.2012
comment
Как? Виртуальные методы можно было вызывать только через общедоступный, не виртуальный интерфейс, который не зависит от точного типа класса. Виртуальные вызовы могут быть встроены только в самой библиотеке, но это, очевидно, не проблема. - person p12; 19.02.2012
comment
@ grumm143: А, тогда вы имели в виду, что определения общедоступных методов не раскрываются (чтобы предотвратить встраивание). - person Matthieu M.; 20.02.2012
comment
Конструктор класса Derived заменит указатель функции по этому смещению переопределением Derived, конструктор не связывается с виртуальной таблицей. На самом деле vtable являются константами (но может потребоваться исправление динамическим компоновщиком во время загрузки). Виртуальная таблица никогда не изменялась динамически. - person curiousguy; 07.08.2012
comment
@grumm143 Вот почему виртуальные методы всегда должны быть приватными. Обнимаю? - person curiousguy; 07.08.2012
comment
@curiousguy: правильно, плохо истолковано, изменился v-указатель; Я добавил уточнение. - person Matthieu M.; 07.08.2012

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

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

person Mark B    schedule 21.04.2011
comment
Да, если компилятор смог девиртуализовать несколько вызовов функций:/ - person Matthieu M.; 21.04.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

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

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

person Marc Mutz - mmutz    schedule 21.04.2011
comment
Спасибо, что поделились этой информацией. На мой взгляд, Марка предложение использовать g++ -fdump-class-hierarchy было бы здесь победителем сразу после проведения надлежащих регрессионных тестов;) - person sehe; 26.04.2011

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

Что ж, ваша интуиция явно неверна:

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

Какой из них верный, зависит от многих факторов.

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

person curiousguy    schedule 07.08.2012

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

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


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

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

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

person sehe    schedule 21.04.2011
comment
_Переопределение не должно изменять макет vtable Неправильно. Это зависит. - person curiousguy; 07.08.2012
comment
@curiousguy Я думал, что ясно дал понять. Также я ссылаюсь на соответствующие ресурсы, чтобы вынести вердикт на основе фактического контекста. Потому что... это зависит от фактического контекста. - person sehe; 07.08.2012
comment
Я думал, что ясно выразился. Мне непонятно, извините. Вы говорите, что для одиночного наследования добавление переопределения не меняет макет виртуальной таблицы? - person curiousguy; 07.08.2012
comment
@curiousguy да, как я понимаю. Если это неверно, почему бы вам не предоставить ответ с описанием таких случаев, чтобы мы могли проголосовать за него? - person sehe; 08.08.2012