Заблокирован
1

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

23.11.2011, 01:05. Показов 13176. Ответов 101
Метки нет (Все метки)

Author24 — интернет-сервис помощи студентам
Наткнулся в интернете на любопытный код.
Спешу поделиться с сообществом.
Просто, что бы кто если не в курсе - узнал, и не попал на эти грабли:


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
Лучшие ответы (1)
Programming
Эксперт
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
23.11.2011, 01:05
Ответы с готовыми решениями:

Неочевидные особенности выдачи pg таблиц
Я довольно много копаюсь в pg_* таблицах, дабы получать информацию о базе данных и прочих системных...

Неочевидные результаты очевидных css-свойств
Добрый вечер, господа верстальщики! Столкнулся с такой неочевидной для меня проблемой:...

Грабли с кодировкой
Здравствуйте! В конструкторе главного окна: MainWindow::MainWindow(QWidget *parent) : ...

Xmega грабли
Так понимаю, мало кто в форуме xmega занимается, но вдруг кому полезно будет. Они, конечно, описаны...

101
Эксперт С++
1069 / 848 / 60
Регистрация: 30.04.2011
Сообщений: 1,659
23.11.2011, 10:49 21
Author24 — интернет-сервис помощи студентам
Цитата Сообщение от Bers Посмотреть сообщение
Вот вы и объясните почему?
Спустимся немного вглубь.
Все указатели на уровне реализации - абсолютно одинаковы. Поэтому явное преобразование типа указателя - это просто для порядку на уровне С++. На самом деле никаких преобразований не производится. Как и было в С.
А вот при наличии в классе виртуальных функциях нужно знать тип ВО ВРЕМЯ ВЫПОЛНЕНИЯ. Чтобы вызвать нужную функцию. Это довольно сложная операция. Это и есть механизм RTTI.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 10:50 22
Лучший ответ Сообщение было отмечено как решение

Решение

Цитата Сообщение от Bers Посмотреть сообщение
Вот вы и объясните почему?
Позволь я объясню. Но это объяснение подходит только для компилятора MSVC, т.к. реализация виртуальных функций никак не регламентирована и реализуется разработчиками по своему усмотрению.
Итак MSVC:
В начале каждого объекта, содержащего виртуальные функции, записывается адрес используемой им таблицы виртуальных функций. Адрес объекта совпадает с указателем на эту таблицу. Виртуальные функции добавляются в эту таблицу в том же порядке, в котором они объявлены в классе. Вызов происходит не напрямую по адресу, а через индекс этой функции. Т.е. вместо непосредственного использования адреса функции, происходит индексированный доступ к таблице и вызов функции по соответствующему адресу.
При множественном наследовании всё усложняется. Для первого базового класса указатель по прежнему записан в самом начале, затем идут данные-члены этого класса и только потом указатель на таблицу второго базового класса. Когда ты выполняешь приведение типа с помощью dynamic_cast, адрес объекта изменяется, т.к. он переносится на "начало" объекта второго базового класса. Ты можешь это легко проверить, выводя адрес объекта после кастования.
Используя reinterpret_cast (круглые скобки это он и есть в данном случае), ты интерпретируешь указатель на один объект, как указатель на другой. Программа бы могла упасть, но прототипы функций одинаковые, поэтому порчи стека не происходит. Данные объекта тоже не используются. Но происходит индексированый доступ функции. Что ты и видишь в консоли.
Можешь добавить в классы поля и попробывать их выводить в консоль (но не изменять) и зразу увидишь, что там не то, что ты ожидаешь.

И это не грабли полиморфизма, это некорректная программа. Не зря ведь говорят ЗАБУДЬТЕ ПРО ПРИВЕДЕНИЕ ТИПОВ В СТИЛЕ Си.
4
Эксперт С++
1069 / 848 / 60
Регистрация: 30.04.2011
Сообщений: 1,659
23.11.2011, 10:50 23
Цитата Сообщение от Bers Посмотреть сообщение
Меня не интересует, есть там перекрестки, или нет. Меня интересует, почему динамик_каст корректно приводит, а приведение в стиле си - не корректно.
И что происходит с объектом, в момент приведения его типа к одному из базовых (срезка).

По русски, с толком, с расстановкой. Не?
Я вам намекну, динамик_каст - дорогое приведение, потому что юзает технологию определения типа объекта в рантайме.
Дык в С - не было классов! И тем более - не было виртуальных функций. И наследования.
Потому и не приводит. Обратная же совместимость!
0
Заблокирован
23.11.2011, 10:51  [ТС] 24
Спустимся:
C++
1
2
aPtr->Func1(); //вывод: A::Func1
 bPtr->Func2(); //вывод: B::Func2

Корректно.

Вы по прежнему щитаете, что между ними нет связи?
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 10:53 25
Цитата Сообщение от Bers Посмотреть сообщение
И что происходит с объектом, в момент приведения его типа к одному из базовых (срезка).
Именно срезка. Но только при приведении типа объекта. При приведении указателя на объект к указателю на базовый тип никакой срезки не происходит. И при ссылке срезки тоже не происходит.
1
Эксперт С++
5043 / 2622 / 241
Регистрация: 07.10.2009
Сообщений: 4,310
Записей в блоге: 1
23.11.2011, 10:54 26
В общем-то насколько мне известно, множественный полиморфизм может быть реализован с использованием нескольких таблиц виртуальных методов (в данном случае двух). Собственно, как выше и говорилось, для этого и нужен dynamic_cast. Если использовать преобразование в стиле Си, то указатель vtable получается фактически случайным из двух.
0
Эксперт С++
1069 / 848 / 60
Регистрация: 30.04.2011
Сообщений: 1,659
23.11.2011, 10:55 27
Цитата Сообщение от Bers Посмотреть сообщение
Спустимся:
C++
1
2
aPtr->Func1(); //вывод: A::Func1
 bPtr->Func2(); //вывод: B::Func2
Корректно.

Вы по прежнему щитаете, что между ними нет связи?
А и В вообще разными программистами в разных файлах могли быть написаны. И транслируются в разное время. Один вчера, а другой - сегодня. ГДЕ тут связь?
Откуда компилятор мог знать, что Берс возьмет эти два класса и множественно унаследует.
А потом явные имена при вызове будет писать и требовать "продолжения банкета" с виртуальными функциями.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 10:55 28
Цитата Сообщение от Bers Посмотреть сообщение
Вы по прежнему щитаете, что между ними нет связи?
Между А и В связи нет.
Но!
Если А* и В* на самом деле указывают на объект типа С, то связь есть. Но только в случае использования dynamic_cast. При желании, можно вручную рассчитать необходимое смещение, сдвинуть указатель и поменять его тип. Но такому желанию лучше не возникать.)
0
Заблокирован
23.11.2011, 11:02  [ТС] 29
ну так вот, возвращаясь к моему тезису:
1. Избегайте множественного наследования + полиморфизм.
2. Избегайте динамического кастования, если дорога производительность.

В противном случае не удивляйтесь ничайному выстрелу себе в ногу.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 11:10 30
Цитата Сообщение от Bers Посмотреть сообщение
ну так вот, возвращаясь к моему тезису:
1. Избегайте множественного наследования + полиморфизм.
2. Избегайте динамического кастования, если дорога производительность.
В противном случае не удивляйтесь ничайному выстрелу себе в ногу.
1 - Нет!
2 - Это миф.
3 - Учитесь программировать и не придётся искать достаточно длинную верёвку, чтобы выстрелить себе в ногу.
2
Заблокирован
23.11.2011, 11:11  [ТС] 31
сказал Deviaphan, ничерта не пролив свет на происходящее
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 11:18 32
Цитата Сообщение от Bers Посмотреть сообщение
ничерта не пролив свет на происходящее
На странице ТРИ я в подробности описал, как работает полиморфизм в С++. Если не желаешь читать, это не мои траблы.

Добавлено через 32 секунды
От тута Неочевидные грабли полиморфизма с++
0
В астрале
Эксперт С++
8049 / 4806 / 655
Регистрация: 24.06.2010
Сообщений: 10,562
23.11.2011, 11:41 33
Deviaphan, На счет динамик каста я бы поспорил. Много динамик кастов убивают производительность. В одной проге профайлер показывает, что динамик каст занимает 14% общего времени, правда используется он там действительно немало где
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 11:51 34
Цитата Сообщение от ForEveR Посмотреть сообщение
В одной проге профайлер показывает, что динамик каст занимает 14% общего времени
Скорее всего ошибка в проектировании. В 80% случаев, повышающий каст означает ошибку проектирования. В 20% программисту удаётся доказать, что это не ошибка.)

И маленький примерчик, для "закрепления материала"
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
class A
{
public:
    virtual void Func1(){cout<<"A::Func1"<<endl;}
};
 
class B
{ 
public:
    virtual void Func2(){cout<<"B::Func2"<<endl;}
};
 
int _tmain(int argc, _TCHAR* argv[])
{
    A * a = new A;
    A * b = (A*)(new B);
 
    a->Func1();
    b->Func1();
 
 
    cin.get();
    return 0;
}
Добавлено через 1 минуту
Можно закомментировать слова virtual, чтобы совсем уж точно материал усвоить.)
2
4226 / 1795 / 211
Регистрация: 24.11.2009
Сообщений: 27,562
23.11.2011, 11:54 35
Цитата Сообщение от Bers Посмотреть сообщение
#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;
}
Не вижу граблей, а вот если бы компилятор проглотил void*, то это и были бы грабли.
0
ForEveR
23.11.2011, 11:56
  #36

Не по теме:

Deviaphan, http://www.jezuk.co.uk/cgi-bin... howtobuild при желании можешь посмотреть сорцы. Мб это и ошибка в проектировании

0
4226 / 1795 / 211
Регистрация: 24.11.2009
Сообщений: 27,562
23.11.2011, 12:01 37
Цитата Сообщение от Сыроежка Посмотреть сообщение
вывод: A::Func1 Почему?
Потому что это не потомок A. Ты тупо переназначил тип самому адресу, то есть сказал машине, что адрес теперь просто целое и сразу: "Нет, я передумал, это адрес B", и вызвал функцию по определённому смещению в таблице виртуальных функций, но таблица то по этому указателю соответствует классу A и по такому смещению там валяется указатель на функцию A::Func1().
C++
1
2
float x;
std::cout<<*((int*)(&x)); // Вывод бредовый.  Грабли? Нет. Так просто не делают, если только не хотят специально вывести в десятичном формате код икса, а не его значение.
Автор твоих "не очевидных граблей" просто считает всех вокруг идиотами, не способными понять смысл своих действий.
0
Deviaphan
23.11.2011, 12:04
  #38

Не по теме:

Цитата Сообщение от ForEveR Посмотреть сообщение
б это и ошибка в проектировании
Преобразование базового типа к дочернему (почти) равнозначно переключению типов. А в большинстве случаев это ошибка проектирования либо интерфейса базового класса либо всей архитектуры в целом. Разумеется, в некоторых случаях это насущная необходимость, но тогда и о падении производительности речи быть не может, другого варианта всё равно нет.) В случае, если использовалось множественное наследование.
Копаться в исходниках XML парсера особого желания не возникает.:)

1
4226 / 1795 / 211
Регистрация: 24.11.2009
Сообщений: 27,562
23.11.2011, 12:46 39
Цитата Сообщение от Bers Посмотреть сообщение
Ни о чем таком не говорит?
Это говорит о наследовании как таковом, а множественное - это когда и указатели на два разных базовых класса могут указывать на одного и того же потомка, но не друг на друга, то есть когда одновременно:
C++
1
2
3
4
A *p1=new B; // не допустимо
B *p2=new A; // не допустимо
A *p3=new C; // допустимо
B *p4=new C; // допустимо
. У тебя и это есть, а полиморфизм - это вообще возможность перекрывать методы предка, что также имеется в приведённом исходнике.

Добавлено через 7 минут
Цитата Сообщение от Bers Посмотреть сообщение
И потом, класс C здесь - это самый дальний потомок.
О какой дальности может идти речь при одном уровне наследования? C - это ближайший потомок из всех возможных, а здесь - единственный.

Добавлено через 1 минуту
Цитата Сообщение от Сыроежка Посмотреть сообщение
dynamic_cast используется для приведения указателя на базовый класс к указателю на производный класс. При противоположном преобразовании можно использовать обычное присваивание, так как происходит неявное преобразование.
Вывод говорит об обратном.

Добавлено через 1 минуту
Цитата Сообщение от taras atavin Посмотреть сообщение
классу A
Точнее классу C, но начало таблицы класса C совпадает с таблицей класса A.

Добавлено через 9 минут
Цитата Сообщение от Bers Посмотреть сообщение
Указатель может быть типа int.
Указателей на int не бывает
наоборот.
C++
1
2
int x;
int *p;
x типа int, но x не указатель, p указатель, но p не типа int. Как раз p и есть указатель на int. "Указатель на тип", "указатель на класс" означает, что:
1. Это указатель.
2. Ему разрешено указывать только на данные определённого типа, или только на на данные определённого класса - того типа/класса, который поминается в словосочетании.
Чтоб не сбивать тебя с толку следовало бы говорить "указатель на данное типа", "указатель на объект класса", но ни кто так длинно не говорит, все сокращают и все понимают, что на саму абстракцию указатель не указывает.

Добавлено через 3 минуты
Цитата Сообщение от Bers Посмотреть сообщение
объект типа int
Не бывает объектов типа int. int не класс, а скалярный тип, переменные этого типа - не объекты.

Добавлено через 3 минуты
Цитата Сообщение от Bers Посмотреть сообщение
Потому что между типом самого указателя, и типом объекта, куда он смотрит жёсткой связи не существует.
И куда ж она делась то? У всех она есть, только в твоей голове как то проделитилась. Может потому у тебя и падает всё, что с указателями, что ты не отличаешь int* от void*?

Добавлено через 2 минуты
Цитата Сообщение от Сыроежка Посмотреть сообщение
класс - это тип
Согласен. Класс - это тип, в котором разрешены любые члены, как члены-данные (поля), так и члены-функции (методы), а на некоторых языках (в том числе c++) даже члены-операторы. Но всё таки тип, а не константа.

Добавлено через 6 минут
Цитата Сообщение от Bers Посмотреть сообщение
А вот указатель на объект, или функцию, или функцию-член - звучит однозначно.
Как раз неопределённо. Указатель на объект. На какой, простите, объект? На дом? На корабль? На кирпич? А может на домну? Или на пользователя? Прямо так, на настоящего пользователя в реале? А может на машинное представление данных об объекте? Эйси, всегда это подразумевается. Но о каком объекте? О Солнечной Системе? Или о пылинке? А может о стене? Пока не указан тип, не понятно о каком указателе речь, поэтому "указатель на объект" семантически идентично "указатель", прямо так, одним словом. Слово "объект" просто лишнее, оно не несёт ни какой смысловой нагрузки, так как его абстракция запредельна и не лезет ни в какие рамки, кроме как в тексте на метаязыке о самом понятии "объект" и прибамбасах для поддержки объектной ориентированности. И только когда указан класс, объект принимает хоть какие то разумные очертания, но не раньше.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 12:51 40
Цитата Сообщение от taras atavin Посмотреть сообщение
поэтому "указатель на объект" семантически идентично "указатель"
Противоречишь сам себе. Т.к.
Цитата Сообщение от taras atavin Посмотреть сообщение
Не бывает объектов типа int. int не класс, а скалярный тип, переменные этого типа - не объекты.
Соответственно есть "два типа указателей" На объекты и на скаляры. А ещё есть указатели на массивы (они же на строки). Поэтому уточнение на что конкретно указывает указатель может быть не лишним.
Надо же было твой пост разбить на два.
1
23.11.2011, 12:51
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
23.11.2011, 12:51
Помогаю со студенческими работами здесь

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

Наступлю на те же грабли?
Здравствуйте. Регистрировал доменное имя через jino.ru, а они согласно whois использовали...

Грабли с WM_DEVICECHANGE
Потратил кучу времени на изучение структур связанных с WM_DEVICECHANGE. Все работает как надо...

Грабли malloc/free
С динамической памятью впервой работаю, от сюда и грабли Есть структура typedef struct {...


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

Или воспользуйтесь поиском по форуму:
40
Ответ Создать тему
Опции темы

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru