Вопрос о виртуальном наследовании интерфейсов.
От: Rakafon Украина http://rakafon.blogspot.com/
Дата: 05.10.09 16:03
Оценка:
Добрый день коллеги!
Есть вопрос о виртуальном наследовании интерфейсов.

Вот код функции main, дальше опишу классы:
int _tmain(int argc, _TCHAR* argv[])
{
    IInterface1* obj = new ObjStruct;
    obj->AddRef();
    obj->SomeMethod1();

    IInterface2* obj2 = dynamic_cast<IInterface2*>(obj);
    obj2->AddRef();
    obj2->SomeMethod2();

    obj2->Release();
    obj->Release();

    std::cout << "sizeof(IBase) = " << sizeof(IBase) << " bytes. "<< std::endl;
    std::cout << "sizeof(IInterface1) = " << sizeof(IInterface1) << " bytes. "<< std::endl;
    std::cout << "sizeof(IInterface2) = " << sizeof(IInterface2) << " bytes. "<< std::endl;
    std::cout << "sizeof(ObjStruct) = " << sizeof(ObjStruct) << " bytes. "<< std::endl;

    return 0;
}


Допустим есть следующая иерархия классов:

struct IBase
{
    virtual void AddRef() = 0;
    virtual void Release() = 0;
};

struct IInterface1 : public IBase
{
    virtual void SomeMethod1() = 0;
};

struct IInterface2 : public IBase
{
    virtual void SomeMethod2() = 0;
};

struct ObjStruct
    : public IInterface1
    , public IInterface2
{
    void AddRef() { /*std::cout << "AddRef();" << std::endl;*/ };
    void Release() { /*std::cout << "Release();" << std::endl;*/ };
    void SomeMethod1() { /*std::cout << "SomeMethod1();" << std::endl;*/ };
    void SomeMethod2() { /*std::cout << "SomeMethod2();" << std::endl;*/ };
};

Во время выполнения функции main, вывод следующий:
sizeof(IBase) = 4 bytes.
sizeof(IInterface1) = 4 bytes.
sizeof(IInterface2) = 4 bytes.
sizeof(ObjStruct) = 8 bytes.
Press any key to continue . . .

... ну вроде как всё понятно ... IInterface1 — таблица функций, IInterface2 — таблица функций, объект наследует два интерфейса, получает две таблицы, соответственно два указателя. В каждой таблице, походу, будет три записи. И насколько я понимаю и в одной и в другой таблицах указатели на методы ObjStruct::AddRef() и ObjStruct::Release() будут указывать на одни и те же места в памяти.

Вот ...
Ежели сделать как следует: то есть повторяющийся базовый класс сделать виртуальным, ну типа чтобы он не повторялся в последнем наследнике дважды, то:
( ... для наглядности начинка интерфейсов опущена ... )
struct IBase { };
struct IInterface1 : virtual public IBase { };
struct IInterface2 : virtual public IBase { };
struct ObjStruct
    : public IInterface1
    , public IInterface2
{ };

Во время выполнения функции main, вывод следующий:
sizeof(IBase) = 4 bytes.
sizeof(IInterface1) = 12 bytes.
sizeof(IInterface2) = 12 bytes.
sizeof(ObjStruct) = 20 bytes.
Press any key to continue . . .


Ну вот и что это такое? Никак дупля не поймаю. Откуда у IInterface1 и IInterface2 по 12 байтов внутрях? По три таблицы что ли? Тогда три таблицы на что? А у ObjStruct откуда тогда 20 байт? Получается пять таблиц что ли (4*5=20)? Опять же: на что указывают эти пять указателей? Ну ладно, на таблицу IBase, на таблицу IInterface1, на таблицу IInterface2, а ещё два куда смотрят?

При этом если добавить виртуальность и во время наследования интерфейсов IInterface1 и IInterface2:
struct IBase { };
struct IInterface1 : virtual public IBase { };
struct IInterface2 : virtual public IBase { };
struct ObjStruct
    : virtual public IInterface1
    , virtual public IInterface2
{ };

Во время выполнения функции main, вывод следующий:
sizeof(IBase) = 4 bytes.
sizeof(IInterface1) = 12 bytes.
sizeof(IInterface2) = 12 bytes.
sizeof(ObjStruct) = 24 bytes.
Press any key to continue . . .


То есть получается само по себе виртуальное наследование добавляет ещё один указатель в объект? И что это за зверюга такая, на которую указывает энтот указатель?

Представим, что интерфейсы IBase, IInterface1 и IInterface2 — есть некие библиотечные сущности, которые поменять нельзя. Имеет ли тогда вообще какой-то смысл следующее виртуальное наследование:
struct IBase { };
struct IInterface1 : public IBase { };
struct IInterface2 : public IBase { };
struct ObjStruct
    : virtual public IInterface1
    , virtual public IInterface2
{ };

Во время выполнения функции main, вывод следующий:
sizeof(IBase) = 4 bytes.
sizeof(IInterface1) = 4 bytes.
sizeof(IInterface2) = 4 bytes.
sizeof(ObjStruct) = 12 bytes.

И опять блин, левые четыре байтика как плата за виртуальное наследование ... Рецепторы на известном месте мне подсказывают, что виртуальное наследование интерфейсов IInterface1 и IInterface2 структурой ObjStruct лишено всякого разумного смысла, потому что от ObjStruct никто не наследуется ... Я прав?

Ну а теперь внимание! Самый главный вопрос!
Нужно ли вообще в вышеописанной иерархии интерфейсов/классов использовать виртуальное наследование? Ведь всё так красиво при обычном наследовании! Не прячет ли в самом первом примере двойное невиртуальное наследование структуры IBase каких-то подводных камней? Может виртуальное наследование имеет смысл только при наследовании реальных классов, содержащих некие данные, а в случаях когда одна и та же абсолютно абстрактная структура наследуется несколько раз виртуальное наследование и не нужно вовсе? Как мне корректно отнаследоваться в вышеописанном случае?

P.S.: ... за всю свою практику с виртуальным наследованием интерфейсов столкнулся впервые (ну или может не замечал ранее ), и книжки Страуструпа как назло под рукой нету

P.P.S.: Заранее спасибо всем отозвавшимся.
"Дайте мне возможность выпускать и контролировать деньги в государстве и – мне нет дела до того, кто пишет его законы." (c) Мейер Ансельм Ротшильд , банкир.
virtual inheritance
Re: Вопрос о виртуальном наследовании интерфейсов.
От: Erop Россия  
Дата: 05.10.09 18:02
Оценка:
Здравствуйте, Rakafon, Вы писали:

R>Ну а теперь внимание! Самый главный вопрос!

R>Нужно ли вообще в вышеописанной иерархии интерфейсов/классов использовать виртуальное наследование? Ведь всё так красиво при обычном наследовании! Не прячет ли в самом первом примере двойное невиртуальное наследование структуры IBase каких-то подводных камней? Может виртуальное наследование имеет смысл только при наследовании реальных классов, содержащих некие данные, а в случаях когда одна и та же абсолютно абстрактная структура наследуется несколько раз виртуальное наследование и не нужно вовсе? Как мне корректно отнаследоваться в вышеописанном случае?

R>P.S.: ... за всю свою практику с виртуальным наследованием интерфейсов столкнулся впервые (ну или может не замечал ранее ), и книжки Страуструпа как назло под рукой нету


1) IMHO, в вопросах такого рода некисло бы указывать компилятор. Я так понимаю, что речь идёт о MSVC?
2) При виртуальном наследовании всё становится немного хитро. Дело в том, что объект IBase лежит не внутри IInterface1 и IInterface2, как при обычном наследовании, а отдельно. Соответственно оно нуждается в отдельном vptr.
3) С точки зрения эффективности, IMHO, это всё не очень важно. Намного важнее то, что кститься от IBase к IInterfaceN и обратно надо через виртуальную таблицу/RTTI...
4) При этом надо понимать, что семантика всё-таки разная, Например
bool isTheSameObject( IInterface1 * p1, IInterface2 p2) { return p1 == (IBase*)p2; }
будет работать по разному...
Но, в целом я согласен, что в любом раскладе корректнее писать так:
bool isTheSameObject( IInterface1 * p1, IInterface2 p2) { return dynamic_cast<void*>( p1 ) == dynamic_cast<void*>( p2 ); }

5) IMHO, самый важный вопрос тут, это зачем вообще тебе понадобилось наследование интерфейсов? От чего бы не иметь у IBase какой--то варинат QueryInterface, а у IInterfaceN иметь virtual IBase* GetBase()? IMHO, с такими интерфейсами можно работать намного прямее и меньше зависеть от особенностей реализации...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re: Вопрос о виртуальном наследовании интерфейсов.
От: Andrew S Россия http://alchemy-lab.com
Дата: 05.10.09 18:19
Оценка: 1 (1)
R>
R>struct IBase { };
R>struct IInterface1 : virtual public IBase { };
R>struct IInterface2 : virtual public IBase { };
R>struct ObjStruct
R>    : public IInterface1
R>    , public IInterface2
R>{ };
R>

R>Во время выполнения функции main, вывод следующий:
R>
R>sizeof(IBase) = 4 bytes.
R>sizeof(IInterface1) = 12 bytes.
R>sizeof(IInterface2) = 12 bytes.
R>sizeof(ObjStruct) = 20 bytes.
R>Press any key to continue . . .
R>


R>Ну вот и что это такое? Никак дупля не поймаю. Откуда у IInterface1 и IInterface2 по 12 байтов внутрях? По три таблицы что ли? Тогда три таблицы на что? А у ObjStruct откуда тогда 20 байт? Получается пять таблиц что ли (4*5=20)? Опять же: на что указывают эти пять указателей? Ну ладно, на таблицу IBase, на таблицу IInterface1, на таблицу IInterface2, а ещё два куда смотрят?


Это указатели на таблицу оффсетов для перехода от конечного/промежуточного класса к виртуальным базам. Очевидно, для каждого конечного/промежуточного класса смещение базы на этапе компиляции может быть неизвестно, по аналогии с виртуальным вызовом. Пример приведен на вики
http://en.wikipedia.org/wiki/Virtual_inheritance.

Тут более подробно.
http://www.phpcompiler.org/articles/virtualinheritance.html

По поводу как правильно реализовать то что вы хотите — смотрите ATL, лучше чем там не сделать.
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[2]: Вопрос о виртуальном наследовании интерфейсов.
От: Erop Россия  
Дата: 05.10.09 18:22
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>По поводу как правильно реализовать то что вы хотите — смотрите ATL, лучше чем там не сделать.


Почему не сделать?
Можно, например, подсчёт ссылок вынести в IBase, от которого наследоваться ВСЕГДА ВИРТУАЛЬНО...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[3]: Вопрос о виртуальном наследовании интерфейсов.
От: Andrew S Россия http://alchemy-lab.com
Дата: 05.10.09 19:00
Оценка:
AS>>По поводу как правильно реализовать то что вы хотите — смотрите ATL, лучше чем там не сделать.

E>Почему не сделать?


Потому что не сделать. В смысле, можно пробовать сделать по-другому, в результате все-равно будет примерно это же.

E>Можно, например, подсчёт ссылок вынести в IBase, от которого наследоваться ВСЕГДА ВИРТУАЛЬНО...


Нельзя.

1. Это медленнее (скорость вызова).
2. Это весит больше.
3. Лейаут разный для разных компиляторов — межмодульная совместимость невозможна.
4. На этапе создания интерфейса я не знаю будет он использоваться в ромбовидных иерархиях или нет. Если нет — почему я должен платить за то, что мне не надо?
5. Проблему множественное наследование базы составляет только при реализации, для самих интерфейсов наличие нескольких пустых баз (поскольку имеются в виду абстрактные интерфейсы) никакой проблемы не представляет.

Итого.

Проблема не имеет места быть, если наследовать не реализации, а только интерфейсы. Что, собственно, и демонстрирует ATL.
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[4]: Всё зависит от целей...
От: Erop Россия  
Дата: 05.10.09 19:07
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>Проблема не имеет места быть, если наследовать не реализации, а только интерфейсы. Что, собственно, и демонстрирует ATL.


Зато реализовывать самый выведенный класс неудобно. Особенно неудобно, если у тебя в реализации есть какие-то ортогональные требования на самый выведенный класс. Например их может наложить какая-нибудь фабрика...

С другой стороны, если независимость от компилятора не нужна, то зачем химичить не ясно.
С другой стороны, если речь идёт о подсчёте ссылок, то накладные расходы на виртуальную базу (лишний указатель + чуть подороже касты) не такие уж и заметные
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[4]: Вопрос о виртуальном наследовании интерфейсов.
От: Erop Россия  
Дата: 05.10.09 19:09
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>1. Это медленнее (скорость вызова).

Кстати, а почему это (скорость вызова) медленнее?
Казалось бы, тот же vptr, на ту же thank-функцию...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[5]: Всё зависит от целей...
От: Andrew S Россия http://alchemy-lab.com
Дата: 05.10.09 19:14
Оценка:
AS>>Проблема не имеет места быть, если наследовать не реализации, а только интерфейсы. Что, собственно, и демонстрирует ATL.

E>Зато реализовывать самый выведенный класс неудобно. Особенно неудобно, если у тебя в реализации есть какие-то ортогональные требования на самый выведенный класс. Например их может наложить какая-нибудь фабрика...


Очень удобно. По мне — не менее удобно, чем в случае виртуального наследования. Тем более, что обычно реализация абстрактного интерфейса сводится к банальному вызову реализации со статическим интерфейсом.

E>С другой стороны, если независимость от компилятора не нужна, то зачем химичить не ясно.


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

E>С другой стороны, если речь идёт о подсчёте ссылок, то накладные расходы на виртуальную базу (лишний указатель + чуть подороже касты) не такие уж и заметные


Не сами касты, а вызовы методов промежуточных и базовых классов. Вот тут начинается настоящая филейная часть...
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[5]: Вопрос о виртуальном наследовании интерфейсов.
От: Andrew S Россия http://alchemy-lab.com
Дата: 05.10.09 19:17
Оценка: :)
AS>>1. Это медленнее (скорость вызова).
E>Кстати, а почему это (скорость вызова) медленнее?
E>Казалось бы, тот же vptr, на ту же thank-функцию...

В случае невиртуального наследования смещения базы/промежуточных классов известны на этапе компиляции. Смещение this корректируется бесплатно.
А чтобы скорректировать значение this в случае виртуального наследования, очевидно, надо и сделать каст через еще одну виртуальную таблицу. Посмотрите статью про размер указателя на функцию, там есть алгоритм вызова.
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[6]: Всё зависит от целей...
От: Erop Россия  
Дата: 05.10.09 19:40
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>Очень удобно. По мне — не менее удобно, чем в случае виртуального наследования. Тем более, что обычно реализация абстрактного интерфейса сводится к банальному вызову реализации со статическим интерфейсом.


Это с какого такого перепугу? За интерфейсами стоит какая-то объектная модель, к разным аспектам объектов которой интерфейсы и дают доступ. У сложных программ и объектная модель сложная, так что ограничения на её устройство -- не всегда классно...

AS>Нужна производительность. И вменяемость лейаута объекта, не только в памяти, но и при, например, конструировании. В накат виртуальное наследование реализации готовит еще несколько сюрпризов с правилами доступа к конструктору базы + неочевидные для обычных людей правила конструирования, особенно, если конструктор базы имеет параметры со значениями по умолчанию.


Я конечно извиняюсь, но зачем это всё для конструктора IBase, если он только подсчётом ссылок занят? Дефолтный конструктор и конструктор копии, которые оба ставят счётчик в 0, плюс оператор присваивания, который ничего не делает -- вот и всё, что нужно. И никаких сюрпризов.

Дальше, когда компилятор создаёт конструктор самого выведенного объекта, весь лэйаут уже известен -- все вызовы невиртуальные.
Конечно, если в каком-то из конструкторов объектов, выведенных из IBase понадобится получить IBase, то понадобится виртуальный вызов, но это какая-то странная нужда. Тем более, что в ATL-подходе доступа к счётчикам из конструктора объекта получить вообще нельзя -- они ещё не сконструированы
А вот IBase gjkexbт уже можно, и что будет, если мы у него позовём AddRef --

E>>С другой стороны, если речь идёт о подсчёте ссылок, то накладные расходы на виртуальную базу (лишний указатель + чуть подороже касты) не такие уж и заметные


AS>Не сами касты, а вызовы методов промежуточных и базовых классов. Вот тут начинается настоящая филейная часть...

Какая именно? И чем она отличается от случая ATL-реализации и множественного наследования?
Вот есть у меня IInterface1 и IInterface2 и CObj, выведенный из них обоих.
Есть метод IInterface2::f, реализованный в CObj.
Как мы его зовём? Да берём из vptr указатель на тайный метод (условно) IInterface2::call_IInterface2_f_from_CObj, который эквивалентен такому коду:
TRes call_IInterface2_f_from_CObj( TArg arg ) { return static_cast<CObj*>(this)->f( arg ); }
При этом у нас есть только один виртуальный вызов + статическая коррекция this + сама функция.
При этом, обращаю твоё внимание, что IInterface2::call_IInterface2_f_from_CObj генериться при порождении таблицы виртуальных функций для самого выведенного класса.
А если у нас виртуальная база, то всё происходит РОВНО ТАК ЖЕ...
Потому как и том и там для самого выведенного класса лэйаут известен на момент компиляции...

В любом случае, если ты уверен, что один подход тормознее другого, то у тебя наверное есть тесты производительности?..


Правда, повторюсь
Автор: Erop
Дата: 05.10.09
, это всё не очень-то и важно, так как, IMHO, в интерфейсах вообще лучше избегать наследования...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[6]: Вопрос о виртуальном наследовании интерфейсов.
От: Erop Россия  
Дата: 05.10.09 19:43
Оценка: -1
Здравствуйте, Andrew S, Вы писали:

AS>А чтобы скорректировать значение this в случае виртуального наследования, очевидно, надо и сделать каст через еще одну виртуальную таблицу. Посмотрите статью про размер указателя на функцию, там есть алгоритм вызова.


Тут вроде бы на "Ты" принято?
А что касается корректирования this, то оно "забесплатно" и тогда и тогда.
Единственная проблема -- это когда мы зовём метод интерфейса, по указателю на метод. Но это, IMHO, редкий довольно случай... Особенно редкий, если при этом ещё и существенны накладные расходы на лишнее разыменование указателя на виртуальную таблицу...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[2]: Вопрос о виртуальном наследовании интерфейсов.
От: Кодт Россия  
Дата: 05.10.09 20:04
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>Тут более подробно.

AS>http://www.phpcompiler.org/articles/virtualinheritance.html

Как раз "тут" показано, как в vtable можно запихать всё необходимое.
В результате лэяуты промежуточных классов должны содержать только свой vfptr (откуда из vtable берётся смещение до виртуальной базы) и собственно тело виртуальной базы, содержащее её vfptr — итого 8, а не 12.
А лэяут финального класса — содержит тела промежуточных классов (1 vfptr в каждом), наследуя первый из них для своих нужд, и тело виртуальной базы. Итого 12.

Но MSVC пошло по более расточительному в плане памяти, но более быстрому пути: каждый виртуально унаследованный класс хранит смещение до виртуальной базы прямо в теле, а не в таблице. Отсюда (2+1)*4 = 12 байтов в промежуточных и (2+2+1)*4 = 20 в финальном.
Перекуём баги на фичи!
Re: Вопрос о виртуальном наследовании интерфейсов.
От: Rakafon Украина http://rakafon.blogspot.com/
Дата: 06.10.09 08:41
Оценка:
Здарова!
Ого за вечер успели ответить! Круто!
Почитал вас, почитал Страуструпа, ... и всё стало на свои места :)
"Дайте мне возможность выпускать и контролировать деньги в государстве и – мне нет дела до того, кто пишет его законы." (c) Мейер Ансельм Ротшильд , банкир.
Re[3]: Вопрос о виртуальном наследовании интерфейсов.
От: Andrew S Россия http://alchemy-lab.com
Дата: 06.10.09 19:05
Оценка:
AS>>Тут более подробно.
AS>>http://www.phpcompiler.org/articles/virtualinheritance.html

К>Как раз "тут" показано, как в vtable можно запихать всё необходимое.

К>В результате лэяуты промежуточных классов должны содержать только свой vfptr (откуда из vtable берётся смещение до виртуальной базы) и собственно тело виртуальной базы, содержащее её vfptr — итого 8, а не 12.

К>А лэяут финального класса — содержит тела промежуточных классов (1 vfptr в каждом), наследуя первый из них для своих нужд, и тело виртуальной базы. Итого 12.


К>Но MSVC пошло по более расточительному в плане памяти, но более быстрому пути: каждый виртуально унаследованный класс хранит смещение до виртуальной базы прямо в теле, а не в таблице. Отсюда (2+1)*4 = 12 байтов в промежуточных и (2+2+1)*4 = 20 в финальном.


Пример показывает, что это не так (полный пример см. выше по ветке):


; 1418 :         pa2->foo();

    mov    eax, DWORD PTR _pa2$[esp+4]
    add    esp, 4
    mov    ecx, DWORD PTR [eax+4]  ; ecx = AA2 offset vtable address
    mov    edx, DWORD PTR [ecx+4]  ; edx = AA1 offset from AA2 offset vtable address
    lea    ecx, DWORD PTR [edx+eax+4] ; ecx = AA1_subojbect_this == AA2 address  + offset
    mov    eax, DWORD PTR [edx+eax+4] ; eax = AA1 subobject vtable address.
    call    DWORD PTR [eax]            ; call of the AA1::foo.
    pop    esi


Статья также говорит о другом.

http://www.lrdev.com/lr/c/virtual.html

ну и это.

http://www.devdoc.ru/index.php/content/view/virtual_inheritance.htm
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[7]: Всё зависит от целей...
От: Andrew S Россия http://alchemy-lab.com
Дата: 06.10.09 19:09
Оценка:
AS>>Очень удобно. По мне — не менее удобно, чем в случае виртуального наследования. Тем более, что обычно реализация абстрактного интерфейса сводится к банальному вызову реализации со статическим интерфейсом.

E>Это с какого такого перепугу? За интерфейсами стоит какая-то объектная модель, к разным аспектам объектов которой интерфейсы и дают доступ. У сложных программ и объектная модель сложная, так что ограничения на её устройство -- не всегда классно...


Ограничений как раз тут нет.

AS>>Нужна производительность. И вменяемость лейаута объекта, не только в памяти, но и при, например, конструировании. В накат виртуальное наследование реализации готовит еще несколько сюрпризов с правилами доступа к конструктору базы + неочевидные для обычных людей правила конструирования, особенно, если конструктор базы имеет параметры со значениями по умолчанию.


E>Я конечно извиняюсь, но зачем это всё для конструктора IBase, если он только подсчётом ссылок занят? Дефолтный конструктор и конструктор копии, которые оба ставят счётчик в 0, плюс оператор присваивания, который ничего не делает -- вот и всё, что нужно. И никаких сюрпризов.


Мы говорим про реализацию IBase, которую будут наследовать.

E>Дальше, когда компилятор создаёт конструктор самого выведенного объекта, весь лэйаут уже известен -- все вызовы невиртуальные.


E>Конечно, если в каком-то из конструкторов объектов, выведенных из IBase понадобится получить IBase, то понадобится виртуальный вызов, но это какая-то странная нужда. Тем более, что в ATL-подходе доступа к счётчикам из конструктора объекта получить вообще нельзя -- они ещё не сконструированы

E>А вот IBase gjkexbт уже можно, и что будет, если мы у него позовём AddRef --

Ничего не понял, но уверен, что мимо

E>>>С другой стороны, если речь идёт о подсчёте ссылок, то накладные расходы на виртуальную базу (лишний указатель + чуть подороже касты) не такие уж и заметные


AS>>Не сами касты, а вызовы методов промежуточных и базовых классов. Вот тут начинается настоящая филейная часть...

E>Какая именно? И чем она отличается от случая ATL-реализации и множественного наследования?
E>Вот есть у меня IInterface1 и IInterface2 и CObj, выведенный из них обоих.
E>Есть метод IInterface2::f, реализованный в CObj.
E>Как мы его зовём? Да берём из vptr указатель на тайный метод (условно) IInterface2::call_IInterface2_f_from_CObj, который эквивалентен такому коду:
TRes call_IInterface2_f_from_CObj( TArg arg ) { return static_cast<CObj*>(this)->f( arg ); }
При этом у нас есть только один виртуальный вызов + статическая коррекция this + сама функция.


Наверное, у вас свой компилятор. В православных там, где нет виртуального наследования, просто корректируется this и вызывается то, что лежит в vtable. А для msvc и для виртуального наследования никаких переходников нет.

E>При этом, обращаю твоё внимание, что IInterface2::call_IInterface2_f_from_CObj генериться при порождении таблицы виртуальных функций для самого выведенного класса.


Ничего там не генерится. Для CObj генерится свои vtable, куда заносится адрес метода, который реализует вызов. Коррекция this осуществляется статически, поскольку на этапе компиляции известно смещение каждого из подобъектов относительно другого.

E>А если у нас виртуальная база, то всё происходит РОВНО ТАК ЖЕ...


Нет.

E>Потому как и том и там для самого выведенного класса лэйаут известен на момент компиляции...


Для не самого выведенного — не известен. Пример в другой ветке я привел.

E>В любом случае, если ты уверен, что один подход тормознее другого, то у тебя наверное есть тесты производительности?..

E>

E>Правда, повторюсь
Автор: Erop
Дата: 05.10.09
, это всё не очень-то и важно, так как, IMHO, в интерфейсах вообще лучше избегать наследования...


Почитайте лучше следующее:
http://www.lrdev.com/lr/c/virtual.html
http://www.devdoc.ru/index.php/content/view/virtual_inheritance.htm
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[7]: Вопрос о виртуальном наследовании интерфейсов.
От: Andrew S Россия http://alchemy-lab.com
Дата: 06.10.09 19:11
Оценка:
AS>>А чтобы скорректировать значение this в случае виртуального наследования, очевидно, надо и сделать каст через еще одну виртуальную таблицу. Посмотрите статью про размер указателя на функцию, там есть алгоритм вызова.

E>Тут вроде бы на "Ты" принято?

E>А что касается корректирования this, то оно "забесплатно" и тогда и тогда.

E>Единственная проблема -- это когда мы зовём метод интерфейса, по указателю на метод. Но это, IMHO, редкий довольно случай... Особенно редкий, если при этом ещё и существенны накладные расходы на лишнее разыменование указателя на виртуальную таблицу...


Нет, это не так. Указатель на метод это лишь производная проблемы.

struct AA1
{
    virtual void foo()
    {
    }
};

struct AA2: virtual public AA1
{
    virtual void foo1()
    {
    }
};

struct AA3: virtual public AA1
{
    virtual void foo2()
    {
    }
};

struct AA4: public AA2, public AA3
{
};


void test(AA1 *pa1, AA2*pa2)
{
    getch();
        printf("sfsdf");

        pa1->foo();

        printf("sfsdf");
        
        pa2->foo();



}


И вот что получается из этого:

;    COMDAT ?test@@YAXPAUAA1@@PAUAA2@@@Z
_TEXT    SEGMENT
_pa1$ = 8
_pa2$ = 12
?test@@YAXPAUAA1@@PAUAA2@@@Z PROC NEAR            ; test, COMDAT

; 1410 : {

    push    esi

; 1411 :     getch();

    call    DWORD PTR __imp__getch

; 1412 :         printf("sfsdf");

    mov    esi, DWORD PTR __imp__printf
    push    OFFSET FLAT:??_C@_05PDMO@sfsdf?$AA@    ; string
    call    esi

; 1413 : 
; 1414 :         pa1->foo();

    mov    ecx, DWORD PTR _pa1$[esp+4]
    add    esp, 4
    mov    eax, DWORD PTR [ecx]
    call    DWORD PTR [eax]

; 1415 : 
; 1416 :         printf("sfsdf");

    push    OFFSET FLAT:??_C@_05PDMO@sfsdf?$AA@    ; string
    call    esi

; 1417 :         
; 1418 :         pa2->foo();

; 1418 :         pa2->foo();

    mov    eax, DWORD PTR _pa2$[esp+4]
    add    esp, 4
    mov    ecx, DWORD PTR [eax+4]  ; ecx = AA2 offset vtable address
    mov    edx, DWORD PTR [ecx+4]  ; edx = AA1 offset from AA2 offset vtable address
    lea    ecx, DWORD PTR [edx+eax+4] ; ecx = AA1_subojbect_this == AA2 address  + offset
    mov    eax, DWORD PTR [edx+eax+4] ; eax = AA1 subobject vtable address.
    call    DWORD PTR [eax]            ; call of the AA1::foo.
    pop    esi

; 1419 : 
; 1420 : 
; 1421 : 
; 1422 : }

    ret    0
?test@@YAXPAUAA1@@PAUAA2@@@Z ENDP            ; test
_TEXT    ENDS


Оба раза this собирается в ecx. Разницу видите?
А все почему — в месте вызова AA2::AA1::foo неизвестно смещение AA1 относительно АА2, именно ввиду того, что АА2 является виртуальной базой, а значит смещение определяется иерархией наследования.
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[8]: Первое и последнее предупреждение.
От: Erop Россия  
Дата: 06.10.09 21:20
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>Ограничений как раз тут нет.

Когда речь идёт о ATL-подходе, то мы говорим о таком подходе, когда мы пишем самый выведенный класс сами, но потом создаём экземпляр не этого класса, а некоего шаблонного, параметризированного нашим, "самым выведенным"? Так?

Если так, то у фабрик, например, бывают аналогичные желания по сходим причинам...


E>>Я конечно извиняюсь, но зачем это всё для конструктора IBase, если он только подсчётом ссылок занят? Дефолтный конструктор и конструктор копии, которые оба ставят счётчик в 0, плюс оператор присваивания, который ничего не делает -- вот и всё, что нужно. И никаких сюрпризов.


AS>Мы говорим про реализацию IBase, которую будут наследовать.

И что? Какая разница наследовать её или реализовывать каждый раз в шаблонном классе? Во втором случае будет много почти дублирующегося кода...

AS>Ничего не понял, но уверен, что мимо

Если ты ещё раз нахамишь, я не буду с тобой больше разговаривать. То, что ты не понял и уверен большого ума не демонстрирует.

Короче, объясни, где именно неэффективность в конструкторе объекта с виртуальной базой?

AS>Ничего там не генерится. Для CObj генерится свои vtable, куда заносится адрес метода, который реализует вызов. Коррекция this осуществляется статически, поскольку на этапе компиляции известно смещение каждого из подобъектов относительно другого.


Как так, статически? Вот есть у меня IInterface2* p. И зову я у ного p->f(). Как происходит коррекция this статически? Откуда вызывающая сторона знает, что IInterface2 не первая база объекта, в котором реализована f?

AS>Нет.

Аргументированно...

AS>Для не самого выведенного — не известен. Пример в другой ветке я привел.

Ну и что, что для самого невыведенного не известен? Какая разница, вообще? Как часть надо из реализаций методов интерфейса лазить к счётчику ссылок объекта?..

E>>В любом случае, если ты уверен, что один подход тормознее другого, то у тебя наверное есть тесты производительности?..


AS>Почитайте лучше следующее:

AS>http://www.lrdev.com/lr/c/virtual.html
AS>http://www.devdoc.ru/index.php/content/view/virtual_inheritance.htm
Я про хамство совершенно серьёзно тебе говорю. Это тебя не красит и дискуссию в профильном форуме тоже...
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[8]: Вопрос о виртуальном наследовании интерфейсов.
От: Erop Россия  
Дата: 06.10.09 21:42
Оценка:
Здравствуйте, Andrew S, Вы писали:


AS>Оба раза this собирается в ecx. Разницу видите?

AS>А все почему — в месте вызова AA2::AA1::foo неизвестно смещение AA1 относительно АА2, именно ввиду того, что АА2 является виртуальной базой, а значит смещение определяется иерархией наследования.

1. Посмотри куда передаётся управление, при виртуальном вызове вот в таком вот случае:
struct C1 {
    int F1;
    virtual int foo() = 0;
};

struct C2 { int F2; };

struct C3 : C2, C1 { 
    int F3;

    virtual int foo() { return F1 + F2 + F3; }
}; 

int testFoo( C1* p ) { return p->foo(); }
int test()
{
    C3 c;
    return testFoo( &c );
}

Я уж и не говорю про то, что если делать подсчёт ссылок в IBase, то методы его осуществляющие могут вообще быть невиртуальными. Главное, чтобы деструктор был виртуальным


Но это всё не важно. Важно вообще не это, а совсем другое -- у тебя наверное есть тесты производительности, котрые показывают, что это всё НАМНОГО медленнее?
Может быть ты ими поделишься?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Re[9]: Первое и последнее предупреждение.
От: Andrew S Россия http://alchemy-lab.com
Дата: 06.10.09 22:02
Оценка:
E>>>Я конечно извиняюсь, но зачем это всё для конструктора IBase, если он только подсчётом ссылок занят? Дефолтный конструктор и конструктор копии, которые оба ставят счётчик в 0, плюс оператор присваивания, который ничего не делает -- вот и всё, что нужно. И никаких сюрпризов.

AS>>Мы говорим про реализацию IBase, которую будут наследовать.

E>И что? Какая разница наследовать её или реализовывать каждый раз в шаблонном классе? Во втором случае будет много почти дублирующегося кода...

Разница в том, что параметры по умолчанию в этом случае имеют значение. Для реализации.

AS>>Ничего не понял, но уверен, что мимо

E>Если ты ещё раз нахамишь, я не буду с тобой больше разговаривать. То, что ты не понял и уверен большого ума не демонстрирует.

А вы сами то прочитайте, что написали.

E>Короче, объясни, где именно неэффективность в конструкторе объекта с виртуальной базой?


AS>>Ничего там не генерится. Для CObj генерится свои vtable, куда заносится адрес метода, который реализует вызов. Коррекция this осуществляется статически, поскольку на этапе компиляции известно смещение каждого из подобъектов относительно другого.


E>Как так, статически? Вот есть у меня IInterface2* p. И зову я у ного p->f(). Как происходит коррекция this статически? Откуда вызывающая сторона знает, что IInterface2 не первая база объекта, в котором реализована f?


Пример иерархии приведите.

AS>>Нет.

E>Аргументированно...

Еще как. Причем, в отличие от вас, я привел конкретный пример с кодом. Где явно видна разница между виртуальным наследованием и отсутствием.

AS>>Для не самого выведенного — не известен. Пример в другой ветке я привел.

E>Ну и что, что для самого невыведенного не известен? Какая разница, вообще? Как часть надо из реализаций методов интерфейса лазить к счётчику ссылок объекта?..

Смотрите код. Читайте статьи. Лучше чем там, я точно не объясню.

E>>>В любом случае, если ты уверен, что один подход тормознее другого, то у тебя наверное есть тесты производительности?..


Есть.
http://www.rusyaz.ru/pr — стараемся писАть по-русски
Re[10]: Первое и последнее предупреждение.
От: Erop Россия  
Дата: 06.10.09 22:10
Оценка:
Здравствуйте, Andrew S, Вы писали:

AS>Разница в том, что параметры по умолчанию в этом случае имеют значение. Для реализации.

Зачем считалке ссылок параметры конструктора?

E>>Как так, статически? Вот есть у меня IInterface2* p. И зову я у ного p->f(). Как происходит коррекция this статически? Откуда вызывающая сторона знает, что IInterface2 не первая база объекта, в котором реализована f?


AS>Пример иерархии приведите.


Иерархия всё та же. IBase, из него выведены IInterface1 и IInterface2, а из IInterface1 и IInterface2 выведен CObj.
IInterfaceN могут быть выведены как виртуально, так и нет. Я так понимаю, обсуждается вопрос о накладных расходах, связанных с виртуальным наследованием?


AS>Еще как. Причем, в отличие от вас, я привел конкретный пример с кодом. Где явно видна разница между виртуальным наследованием и отсутствием.


Там, в случае отсутствия, за одно ещё и множественного нет...

AS>Смотрите код. Читайте статьи. Лучше чем там, я точно не объясню.

Ты понимаешь, что нужда вычислять указатель на IBase возникнет только при вызове его методов? При вызове методов IInterface1 или IInterface2 всё будет как обычно?

E>>>>В любом случае, если ты уверен, что один подход тормознее другого, то у тебя наверное есть тесты производительности?..


AS>Есть.

И?
Все эмоциональные формулировки не соотвествуют действительному положению вещей и приведены мной исключительно "ради красного словца". За корректными формулировками и неискажённым изложением идей, следует обращаться к их автором или воспользоваться поиском
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.