[C++ Visual Studio] порядок методов в vtable

Maurog

накину задачку по поводу, а заодно и поделюсь своим опытом :grin:
задачка актуально только для студий :( так что с гцц плиз не влезать! :grin:
в таблице виртуальных функций студия чаще всего располагает методы в порядке, указанном при объявлении класса, то есть для класса
class A
{
virtual void Foo1;
virtual void Foo2;
virtual void Foo3;
};
виртуальная таблица будет иметь три записи-указателя на методы
vtbl.methods[0] = &Foo1;
vtbl.methods[1] = &Foo2;
vtbl.methods[2] = &Foo3;
Если добавить виртуальный деструктор, то не зависимо от его положения в объявлении класса в таблице он будет иметь индекс 0, остальные методы просто сдвинутся. Такое поведение могли заметить многие.
Однако в некоторых случаях студия может разместить методы в другом относительном порядке, нежели это указано в классе.
Внимание вопрос: приведите такой пример: в определении один метод перед вторым, а в таблице он после указателя на второй метод. Деструкторы по очевидным причинам не рассматриваем.

ava3443

class Base
{
virtual void Foo2;
virtual void Foo1;
};
class A : public Base
{
virtual void Foo1;
virtual void Foo2;
virtual void Foo3;
};

пойдёт такой пример?

Maurog

пойдёт такой пример?
ну я планировал другим опытом поделиться :grin:
но можно и это проанализировать:
сколько виртуальных таблиц здесь? в какой из них порядок функций будет не таким, как в объявлении класса? ну и вряд ли это VS-specific, правда?:)

ava3443

делись своим примером, наверняка он не такой банальный :)

Maurog

наверняка он не такой банальный
он менее банальный, но простой и там даже нет иерархий
весь пример в одном классе :grin:

Maurog

вот оно
уверяю, подобные нюансы не от хорошей жизни мне известны :grin:
есть некая компания, которая предоставляет API в виде dll и интерфейсы (.h). Обратная совместимость достигается путем неудаления старых виртуальных функций и добавлением в конец новых. Так вот однажды они добавили в конец перегрузку виртуальной функции, которая уже раньше существовала. И старое приложение, загрузив их новую длл, стало падать. А падала она из особенности студии группировать методы в vtable по имени.
Был такой интерфейс
class A
{
virtual void F1(bla-bla1);
virtual void F2(bla-bla2);
virtual void F3(bla-bla3);
virtual void F4(bla-bla4);
};
таблица:
vtbl[0] = &F1;
vtbl[1] = &F2;
vtbl[2] = &F3;
vtbl[3] = &F4;
Приложуха вызывала F4 через vtbl[3];
Добавили новый метод F2
class A
{
virtual void F1(bla-bla1);
virtual void F2(bla-bla2);
virtual void F3(bla-bla3);
virtual void F4(bla-bla4);
virtual void F2(bla-bla5);
};
таблица:
vtbl[0] = &F1;
vtbl[1] = &F2;
vtbl[2] = &F2 новый метод;
vtbl[3] = &F3;
vtbl[4] = &F4;

Приложуха пошла через vtbl[3]; и упала (видимо длл стала доставать аргументы метода)
вот такие пироги :grin:

yroslavasako

зачем, зачем изобрели стандарты? Я тебя не осуждаю, я видел разные варианты нарушений стандартов. Но зачем люди это делают?

Whoman-in-white

А что здесь стандарт нарушает?

yroslavasako

Я полагаю, что vtbl - это undefined behaviour. И полагаться на него глупо

Maurog

Я полагаю, что vtbl - это undefined behaviour
подозреваю, ты что-то не так понял
попробую разъяснить этот момент:
Приложуха вызывала F4 через vtbl[3];
в приложении такой код
#include "API.h"
void foo(API* p)// p from thrid-party dll
{
  p->F4;
}
если в эту приложуху погрузить новую длл (с новой версией интерфейса API то она падать начинает
в приложении не используется низкоуровневый доступ к таблице виртуальных функций, все на настоящем C++ =)

Whoman-in-white

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

yroslavasako

Моё знакомство с winAPI ограничено временами студенчества и поверхностно. Насколько я помню, подразумевается, что все ресурсы из dll выгружаются по уникальному строковому идентификатору.

ppplva

Ну, как видишь, добавление метода в класс может сломать обратную совместимость. Не говоря уже о том, что разные компиляторы (и даже, я слышал, разные версии VS) не ABI-совместимы.
Плюсовые библиотеки конечно имеют право на жизнь, но распространять софт в них - не лучшая идея.

bleyman

Я полагаю, что vtbl - это undefined behaviour. И полагаться на него глупо
unspecified. Undefined behaviour это совсем другой зверь, бро!

vik1538

Не ну, а чо вы хотели?! Это примерно тоже самое, как поле новое в класс добавить - бинарная совместимость нарушается.

tamusyav

зачем, зачем изобрели стандарты? Я тебя не осуждаю, я видел разные варианты нарушений стандартов. Но зачем люди это делают?
На мой взгляд, тут скорее надо смотреть не в сторону стандартов, а в сторону хороших практик. Умные люди говорят, что неприватные виртуальные методы (кроме деструктора) лучше не использовать, потому что виртуальный метод используется для реализации определенного поведения; интерфейс же лучше делать невиртуальным.
В данном контексте можно еще добавить, что обращение к функции или переменной по индексу, не имеющему самостоятельного смысла, - вещь очень ненадежная, даже если бы MSVC их расставлял в желаемом порядке.
И еще один момент: , надеюсь, вы не используете наследование от этих классов? Потому что если используете, то про совместимость на уровне ABI лучше забыть.

Whoman-in-white

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

Serab

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

Maurog

а как можно использовать абстрактные классы без наследования от них?
во-первых, я не понял о каких именно проблемах тут фантазируют, даже если мы отнаследуемся от такого класса
во-вторых, от них нет надобности наследоваться, если они лишь предоставляют API:
lib = LoadLibrary("third-party.dll");
auto createFunc = lib.GetProcAddress<FactoryType>("factory_func");
API* p = createFunc;
p->InvokeSomething;
в-третьих, да, мы наследуемся от thrid-party интерфейсов, т.к. через них делаются обратные вызовы
class MyCallback : public ThirdPartyInterface
{
virtual void OnEvent {};
};
API* p = createFunc;
MyCallback c;
p->RegisterCallback(&c);
кстати, для колбеков тоже интересно сделана версионность, там тоже могут появляться методы, но для того, чтобы third-party.dll не вызвало несуществующий метод, мы передаем номер версии API в фабрику:
API* p = createFunc(API_VERSION);
при таком подходе можно подкладывать более свежую длл и на основании версии она будет понимать можно ли вызывать у колбека какой-то метод

apl13

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

Serab

ну бери питон, там все это есть.

apl13

Блин, это же не свойство языка.
Оставить комментарий
Имя или ник:
Комментарий: