Форум программистов, компьютерный форум CyberForum.ru

Неочевидные грабли полиморфизма с++ - C++

Восстановить пароль Регистрация
 
 
Рейтинг: Рейтинг темы: голосов - 52, средняя оценка - 4.79
Bers
Заблокирован
23.11.2011, 01:05     Неочевидные грабли полиморфизма с++ #1
Наткнулся в интернете на любопытный код.
Спешу поделиться с сообществом.
Просто, что бы кто если не в курсе - узнал, и не попал на эти грабли:


C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;
 
struct A
{
    virtual void Func1(){cout<<"A::Func1"<<endl;}
};
struct B
{ 
    virtual void Func2(){cout<<"B::Func2"<<endl;}}
;
struct C: public A, public B
{ 
    virtual void Func3(){cout<<"C::Func3"<<endl;}
};
 
int main()
{
    A *ptr = new C;
    void *ptr1 = new C;
    
    ((A*)ptr)->Func1(); //вывод: A::Func1
    ((B*)ptr)->Func2(); //вывод: A::Func1 Почему?
    ((C*)ptr)->Func3(); //вывод: C::Func3
    
    A* aPtr=dynamic_cast<A*>(ptr);
    B* bPtr=dynamic_cast<B*>(ptr);
    C* cPtr=dynamic_cast<C*>(ptr)
    
    //нельзя кастовать от void*
    //void* ничего не знает ни о каких классах
    //и ни о каких таблицах виртуальных функций
 
    C* cPtr=dynamic_cast<C*>(ptr1); //ошибка компиляции
    //error C2681: void *: недопустимый тип выражения для dynamic_cast
    
    aPtr->Func1(); //вывод: A::Func1
    bPtr->Func2(); //вывод: B::Func2
    cPtr->Func3(); //вывод: C::Func3
 
    delete ptr;
    delete ptr1;
 
    return 0;
}
От себя могу добавить, что разные компиляторы по разному могут реализовывать механизм полиморфизма. Поэтому, при множественном наследовании если что и спасет - только dynamic_cast
Но в любом случае, нужно быть предельно осторожным.

А по сути, полиморфизм + множественное наследование = мина замедленного действия.

Поэтому, при проектировании полиморфных классов, лучше до последнего избегать множественного наследования.
Лучшие ответы (1)
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
23.11.2011, 01:05     Неочевидные грабли полиморфизма с++
Посмотрите здесь:

C++ Использование полиморфизма
Модификатор const Очередные грабли с++? C++
C++ Реализация полиморфизма
C++ иллюстрация полиморфизма
Виды полиморфизма C++ C++
После регистрации реклама в сообщениях будет скрыта и будут доступны все возможности форума.
Сыроежка
Заблокирован
23.11.2011, 01:42     Неочевидные грабли полиморфизма с++ #2
Цитата Сообщение от Bers Посмотреть сообщение
Наткнулся в интернете на любопытный код.
Спешу поделиться с сообществом.
Просто, что бы кто если не в курсе - узнал, и не попал на эти грабли:


C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
using namespace std;
 
struct A
{
    virtual void Func1(){cout<<"A::Func1"<<endl;}
};
struct B
{ 
    virtual void Func2(){cout<<"B::Func2"<<endl;}}
;
struct C: public A, public B
{ 
    virtual void Func3(){cout<<"C::Func3"<<endl;}
};
 
int main()
{
    A *ptr = new C;
    void *ptr1 = new C;
    
    ((A*)ptr)->Func1(); //вывод: A::Func1
    ((B*)ptr)->Func2(); //вывод: A::Func1 Почему?
    ((C*)ptr)->Func3(); //вывод: C::Func3
    
    A* aPtr=dynamic_cast<A*>(ptr);
    B* bPtr=dynamic_cast<B*>(ptr);
    C* cPtr=dynamic_cast<C*>(ptr)
    
    //нельзя кастовать от void*
    //void* ничего не знает ни о каких классах
    //и ни о каких таблицах виртуальных функций
 
    C* cPtr=dynamic_cast<C*>(ptr1); //ошибка компиляции
    //error C2681: void *: недопустимый тип выражения для dynamic_cast
    
    aPtr->Func1(); //вывод: A::Func1
    bPtr->Func2(); //вывод: B::Func2
    cPtr->Func3(); //вывод: C::Func3
 
    delete ptr;
    delete ptr1;
 
    return 0;
}
От себя могу добавить, что разные компиляторы по разному могут реализовывать механизм полиморфизма. Поэтому, при множественном наследовании если что и спасет - только dynamic_cast
Но в любом случае, нужно быть предельно осторожным.

А по сути, полиморфизм + множественное наследование = мина замедленного действия.

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

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

C++
1
2
3
    ((A*)ptr)->Func1(); //вывод: A::Func1
    ((B*)ptr)->Func2(); //вывод: A::Func1 Почему?
    ((C*)ptr)->Func3(); //вывод: C::Func3
то у компилятора нет никаких сведений о том, что в этой памяти находится. Фактически этот код соответствует коду

C++
1
2
    reinterpret_cast<A*>(ptr)->Func1(); //вывод: A::Func1
    reinterpret_cast<B*>(ptr)->Func2(); //вывод: A::Func1 Почему?
То есть, например, для второй строки кода компилятор реинтерпретирует таблицу виртуальных функций, как таблицу виртуальных функций класса B. В классе B всего одна функция. К таблице виртуальных функций компилятор обращается по индексу точно также, как он это делает с массивами. Поэтому совершенно не удивительно, что он вызывает функцию A::Func1, так как ее адрес хранится в таблице виртуальных функций с индексом 0.

Когда же вы тоже самое проделываете для класса C, то компилятор знает, что у этого класса таблица виртуальных функций содержит 3 записи, а потому будет правильно вызывать не только Func3, но и Func2 и Func1.

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

C++
1
2
3
    A* aPtr=dynamic_cast<A*>(ptr);
    B* bPtr=dynamic_cast<B*>(ptr);
    C* cPtr=dynamic_cast<C*>(ptr)
так как можно присваивать указателю на базовый класс указатель на производный класс. В этом заключается смысл наследования, что объекты производных классов являются в то же время объектами базовых классов.

Так что ничего специфического, чтобы можно было говорить о каких-то подводных камнях полиморфизма, в приведенном вами коде нет. Есть лишь безграмотный код. Но любой безграмотный код опасен!
Bers
Заблокирован
23.11.2011, 02:00  [ТС]     Неочевидные грабли полиморфизма с++ #3
Цитата Сообщение от Сыроежка Посмотреть сообщение
Но также бессмыленно делать dynamic_cast и в следующих предложениях
Однако же, динамическое кастование типов корректно.

C++
1
2
B* bPtr=dynamic_cast<B*>(ptr);
bPtr-> Func2(); //вывод: B::Func2
Смысл не очевиден, да?

Добавлено через 3 минуты
Цитата Сообщение от Сыроежка Посмотреть сообщение
То есть если рассматривать ваши манипуляции с указателями, то никакого множественного наследования нет.

C++
1
A *ptr = new C;
ptr имеет тип A, но смотрит на объект типа C

Ни о чем таком не говорит?
Сыроежка
Заблокирован
23.11.2011, 02:06     Неочевидные грабли полиморфизма с++ #4
Цитата Сообщение от Bers Посмотреть сообщение
Однако же, динамическое кастование типов корректно.

C++
1
2
B* bPtr=dynamic_cast<B*>(ptr);
bPtr-> Func2(); //вывод: B::Func2
Смысл не очевиден, да?

Добавлено через 3 минуты



C++
1
A *ptr = new C;
ptr имеет тип A, но смотрит на объект типа C

Ни о чем таком не говорит?
dynamic_cast используется для приведения указателя на базовый класс к указателю на производный класс. При противоположном преобразовании можно использовать обычное присваивание, так как происходит неявное преобразование. Компилятор правильно преобразует указатели.

Что касается данного примера, то я не обратил внимание, что это указатель на A, я лишь посмотрел на правую часть выражения, где создавался объект класса C.

Мне представляется, что поведение этого кода неопределенное. То есть код некорректный. Другое дело, что в стандарте описано много ситуаций, когда компилятор не обязан выдавать никакого диагностического сообщения о некорректности кода.
Bers
Заблокирован
23.11.2011, 02:10  [ТС]     Неочевидные грабли полиморфизма с++ #5
И потом, класс C здесь - это самый дальний потомок.

Привести тип C к типу A, или B - значит сделать срезку объекта.

То есть, если мы сделали приведение объекта C допустим к объекту A, то часть объекта, которая относится к B вылетит в трубу. Что в этот момент произойдёт с таблицей виртуальных функций?

На сайте, где я подглядел этот код, была любопытная ссылка:
http://insidecpp.ru/test-drive/19/

Добавлено через 2 минуты
Цитата Сообщение от Сыроежка Посмотреть сообщение
для приведения указателя на базовый класс к указателю на производный класс.
Я не знаю что такое "указатель на базовый класс". Я знаю что такое "указатель имеющий тип базового класса, который может смотреть куда угодно. Например, на объект имеющий тип любого из потомков типа указателя"

И это нужно учитывать, и конкретизировать.
Сыроежка
Заблокирован
23.11.2011, 02:21     Неочевидные грабли полиморфизма с++ #6
Цитата Сообщение от Bers Посмотреть сообщение
И потом, класс C здесь - это самый дальний потомок.

Привести тип C к типу A, или B - значит сделать срезку объекта.

То есть, если мы сделали приведение объекта C допустим к объекту A, то часть объекта, которая относится к B вылетит в трубу. Что в этот момент произойдёт с таблицей виртуальных функций?

На сайте, где я подглядел этот код, была любопытная ссылка:
http://insidecpp.ru/test-drive/19/

Добавлено через 2 минуты


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

И это нужно учитывать, и конкретизировать.
Что такое указатель на объект типа int знаете? Это уже хорошо! Вот тоже самое означает указатель на базовый класс.

Что касается класса C , то не имеет смысла говорить, что он - самый дальний потомок, так как перечисленные вами классы не входят в одну иерархию. Классы A и B никак между собой не связаны.

Я могу вам порекомендовать почитать книгу Герба Саттера "Решение сложных задач на С++". Там глава 3 посвещена этим вопросом и так и называется "Разработка классов, наследование и полиморфизм". Он рассматривает похожую ситуацию.
Bers
Заблокирован
23.11.2011, 02:31  [ТС]     Неочевидные грабли полиморфизма с++ #7
Цитата Сообщение от Сыроежка Посмотреть сообщение
Что такое указатель на объект типа int знаете? Это уже хорошо! Вот тоже самое означает указатель на базовый класс.

Что такое указатель на объект типа int я знаю.
Как может указатель указывать на класс - этого я не знаю.
Сыроежка
Заблокирован
23.11.2011, 02:36     Неочевидные грабли полиморфизма с++ #8
Цитата Сообщение от Bers Посмотреть сообщение
Что такое указатель на объект типа int я знаю.
Как может указатель указывать на класс - этого я не знаю.
Это точно также, как указатель на int. И класс и int - это типы. Так что выражение корректно.
Bers
Заблокирован
23.11.2011, 02:40  [ТС]     Неочевидные грабли полиморфизма с++ #9
Цитата Сообщение от Сыроежка Посмотреть сообщение
Это точно также, как указатель на int. И класс и int - это типы. Так что выражение корректно.
Указатель указывает на объект
Причем, далеко не факт, что этот объект будит такого же типа, как и указатель.

Он имеет тип, но смотрит всегда на объект

Он может иметь тип базового класса, но смотреть на базовый класс не может.

Указатель может быть типа int.
Указателей на int не бывает
Сыроежка
Заблокирован
23.11.2011, 02:43     Неочевидные грабли полиморфизма с++ #10
Цитата Сообщение от Bers Посмотреть сообщение
Указатель указывает на
C++
1
объект
Он имеет тип, но смотрит всегда на объект

Он может иметь тип базового класса, но смотреть на базовый класс не может.

Указатель может быть типа int.
Указателей на int не бывает
Указатель на int означает, что речь идет о типе указателя. Указатель на объект типа int предполагает, что указатель уже инициализирован адресом этого объекта.

Чтобы вас еще более озадачить, то я вам сообщу, что есть такое выражение, как указатель на член класса, или указатель на функцию.
Bers
Заблокирован
23.11.2011, 02:48  [ТС]     Неочевидные грабли полиморфизма с++ #11
Цитата Сообщение от Сыроежка Посмотреть сообщение
Указатель на int означает, что речь идет о типе указателя. Указатель на объект типа
Ну есть разница:

Указатель на int

и

указатель на объект типа int

?

Я могу сказать "указатель на int" - что это значит? Что тип указателя int, или что он смотрит на объект типа int ?

Потому что между типом самого указателя, и типом объекта, куда он смотрит жёсткой связи не существует. Он может смотреть на объекты любых типов.

Поэтому для указателя очень критично какой тип у самого указателя, и какой на самом деле тип у объекта, на который он указывает. Если это не конкретизировать - можно запросто запутаться. И наступить на грабли.
Сыроежка
Заблокирован
23.11.2011, 02:50     Неочевидные грабли полиморфизма с++ #12
Bers,
Так как я знаю, что вы упрямый, то, чтобы закрыть вопрос, я вам просто приведу выражение из стандарта С++

the pointer is implicitly converted (4.10) to a pointer to a base class type

Но так как я уже предвижу ваши вопросы, то я поясню (что я уже делал ранее), что класс - это тип. Так что в выше указанной фразе слово type можно совершенно спокойно опустить.

Я уж не говорю о том, что в стандарте встречается выражение pointer to int ,без добавления слова type.
Bers
Заблокирован
23.11.2011, 02:55  [ТС]     Неочевидные грабли полиморфизма с++ #13
Цитата Сообщение от Сыроежка Посмотреть сообщение
указатель на член класса
Содержит адрес функции.
По аналогии тоже самое, что сказать "указатель на объект".
При этом тип самого указателя здесь вторичен.

Указатель на класс - звучит как то не правильно. Указатель на тип...

А вот указатель на объект, или функцию, или функцию-член - звучит однозначно.
Точно так же, однозначно звучит "указатель типа...", ну например: "указатель типа int"

Добавлено через 1 минуту

Не по теме:

ладно.. буквоедство это все. Я спать пошёл

Сыроежка
Заблокирован
23.11.2011, 03:06     Неочевидные грабли полиморфизма с++ #14
Цитата Сообщение от Bers Посмотреть сообщение
Содержит адрес функции.
По аналогии тоже самое, что сказать "указатель на объект".
При этом тип самого указателя здесь вторичен.

Указатель на класс - звучит как то не правильно. Указатель на тип...

А вот указатель на объект, или функцию, или функцию-член - звучит однозначно.
Точно так же, однозначно звучит "указатель типа...", ну например: "указатель типа int"

Добавлено через 1 минуту

Не по теме:

ладно.. буквоедство это все. Я спать пошёл

Я вам в своем предыдущем сообщении уже все разъяснил. Просто я его несколько раз корректировал.

Добавлено через 8 минут
Bers,

Чтобы уж совсем никаких вопросов не было, то я приведу определение из стандарта:

A pointer to objects of type T is referred to as a "pointer to T"
ValeryLaptev
Эксперт C++
1005 / 784 / 46
Регистрация: 30.04.2011
Сообщений: 1,595
23.11.2011, 10:30     Неочевидные грабли полиморфизма с++ #15
Цитата Сообщение от Bers Посмотреть сообщение
Наткнулся в интернете на любопытный код.
Спешу поделиться с сообществом.
Просто, что бы кто если не в курсе - узнал, и не попал на эти грабли:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
using namespace std;
 
struct A
{    virtual void Func1(){cout<<"A::Func1"<<endl;}};
struct B
{    virtual void Func2(){cout<<"B::Func2"<<endl;}};
struct C: public A, public B
{     virtual void Func3(){cout<<"C::Func3"<<endl;}};
int main()
{   A *ptr = new C;
    void *ptr1 = new C;
    ((A*)ptr)->Func1(); //вывод: A::Func1
    ((B*)ptr)->Func2(); //вывод: A::Func1 Почему?
    ((C*)ptr)->Func3(); //вывод: C::Func3
    
    A* aPtr=dynamic_cast<A*>(ptr);
    B* bPtr=dynamic_cast<B*>(ptr);
    C* cPtr=dynamic_cast<C*>(ptr)
     
    aPtr->Func1(); //вывод: A::Func1
    bPtr->Func2(); //вывод: B::Func2
    cPtr->Func3(); //вывод: C::Func3
 
    delete ptr;
    delete ptr1;
 
    return 0;
}
От себя могу добавить, что разные компиляторы по разному могут реализовывать механизм полиморфизма. Поэтому, при множественном наследовании если что и спасет - только dynamic_cast
Но в любом случае, нужно быть предельно осторожным.

А по сути, полиморфизм + множественное наследование = мина замедленного действия.

Поэтому, при проектировании полиморфных классов, лучше до последнего избегать множественного наследования.
1. У вас во всех классах - РАЗНЫЕ виртуальные функции.
2. Вы все функции вызываете ЯВНЫМ образом непосредственно по имени.
3. Простое преобразование типа указателя не прокатывает при вызове виртуальных функций. Именно поэтому и был придуман dynamic_cast<>.
4. Вы пытаетесь преобразовать указатель типа A в указатель типа В, который никаким боком с А не связан.
Надеюсь, хоть немного прояснилось?
Множественное наследование с виртуальными функциями - это действительно ОЧЕНЬ аккуратно надо писать.

Добавлено через 4 минуты
Добавлю из своей книжки:
В главе 9 при обсуждении виртуальных методов мы уже упоминали механизм RTTI, с помощью которого можно определить тип объекта во время выполнения, имея только указатель на него. Механизм динамической идентификации типа состоит из трех составляющих:
  • оператора динамического преобразования типа dynamic_cast<> (см. п.п. 5.2.7 в [1]);
  • оператора идентификации точного типа объекта typeid() (см. п.п. 5.2.8 в [1]);
  • класса type_info (см. п.п. 18.5.1 в [1]);
Оператор dynamic_cast<> допускается применять к указателям только на полиморфные классы (содержащие хотя бы одну виртуальную функцию). Вообще-то любой класс легко сделать полиморфным, определив для него виртуальный деструктор. Оператор имеет следующий формат:

dynamic_cast<тип *>(указатель)

Результатом является преобразованный указатель. Если преобразование указателя к нужному типу выполнить не удается, оператор возвращает нулевой указатель. Следовательно, результат преобразования нужно всегда проверять.

Преобразования допускаются только между «родственниками», то есть классами, входящими в одну иерархию наследования (тип указателя в круглых скобках должен быть родственным типу в угловых скобках). Преобразование может быть:
  • повышающим — от производного класса к базовому;
  • понижающим — от базового класса к производному;
  • перекрестным — от одного производного класса к другому.
Как мы знаем, повышающее преобразование выполняется обычно по умолчанию посредством принципа подстановки (см. главу 8). Тем не менее, можно его выполнить и явно. Понижающее и перекрестное преобразования выполняются только с помощью оператора dynamic_cast<>.

Оператор dynamic_cast<> можно применять и к ссылкам. Формат оператора в этом случае такой:

dynamic_cast<тип &>(ссылка)

Все условия относительно «родственности» полиморфных классов должны выполняться и в этом случае, однако обработка аварийного случая происходит по-другому. Так как нулевых ссылок не бывает, при невозможности преобразования ссылки генерируется исключение bad_cast (см. п.п. 18.5.2 в [1]).
[1] - это стандарт С++ 2003
Отсюда следует, что преобразование одного базового в другой - не работает (указатель A преобразовать в B - невозможно)
Bers
Заблокирован
23.11.2011, 10:39  [ТС]     Неочевидные грабли полиморфизма с++ #16
Цитата Сообщение от ValeryLaptev Посмотреть сообщение
3. Простое преобразование типа указателя не прокатывает при вызове виртуальных функций. Именно поэтому и был придуман dynamic_cast<>.
Вот вы и объясните почему?


Цитата Сообщение от ValeryLaptev Посмотреть сообщение
4. Вы пытаетесь преобразовать указатель типа A в указатель типа В, который никаким боком с А не связан.
Связаны общим потомком. Именно поэтому динамическое кастование корректно

Добавлено через 50 секунд
Мой тезис только подтверждаеццо: полиморфизм + множественное наследование = мина замедленного действия.
ValeryLaptev
Эксперт C++
1005 / 784 / 46
Регистрация: 30.04.2011
Сообщений: 1,595
23.11.2011, 10:42     Неочевидные грабли полиморфизма с++ #17
Bers,
Преобразования допускаются только между «родственниками», то есть классами, входящими в одну иерархию наследования (тип указателя в круглых скобках должен быть родственным типу в угловых скобках). Преобразование может быть:
повышающим — от производного класса к базовому;
понижающим — от базового класса к производному;
перекрестным — от одного производного класса к другому.
Вот же - написано.
Нет тут перекрестного преобразования от базового к базовому.
Bers
Заблокирован
23.11.2011, 10:46  [ТС]     Неочевидные грабли полиморфизма с++ #18
Цитата Сообщение от ValeryLaptev Посмотреть сообщение
Вот же - написано.
Нет тут перекрестного преобразования от базового к базовому.
Меня не интересует, есть там перекрестки, или нет. Меня интересует, почему динамик_каст корректно приводит, а приведение в стиле си - не корректно.
И что происходит с объектом, в момент приведения его типа к одному из базовых (срезка).

По русски, с толком, с расстановкой. Не?
Я вам намекну, динамик_каст - дорогое приведение, потому что юзает технологию определения типа объекта в рантайме.
fasked
Эксперт C++
 Аватар для fasked
4925 / 2505 / 180
Регистрация: 07.10.2009
Сообщений: 4,306
Записей в блоге: 1
23.11.2011, 10:46     Неочевидные грабли полиморфизма с++ #19
Извините меня все же, но таки в каком месте здесь грабли неочевидные и камни подводные?
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
23.11.2011, 10:47     Неочевидные грабли полиморфизма с++
Еще ссылки по теме:

В чем смысл полиморфизма C++
C++ Использование свойств полиморфизма
Смысл использования полиморфизма C++

Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
Bers
Заблокирован
23.11.2011, 10:47  [ТС]     Неочевидные грабли полиморфизма с++ #20
не хотите терять производительность - не юзайте динамик_каст. А значит, не юзайте множественное наследование с полиморфизмом. Ваш К. О.
Yandex
Объявления
23.11.2011, 10:47     Неочевидные грабли полиморфизма с++
Ответ Создать тему
Опции темы

Текущее время: 21:46. Часовой пояс GMT +3.
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2016, vBulletin Solutions, Inc.
Рейтинг@Mail.ru