Форум программистов, компьютерный форум, киберфорум
Наши страницы

С++ для начинающих

Войти
Регистрация
Восстановить пароль
 
 
Рейтинг: Рейтинг темы: голосов - 52, средняя оценка - 4.79
Bers
Заблокирован
#1

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

23.11.2011, 01:05. Просмотров 7154. Ответов 100
Метки нет (Все метки)

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


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)
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Similar
Эксперт
41792 / 34177 / 6122
Регистрация: 12.04.2006
Сообщений: 57,940
23.11.2011, 01:05
Здравствуйте! Я подобрал для вас темы с ответами на вопрос Неочевидные грабли полиморфизма с++ (C++):

Модификатор const Очередные грабли с++? - C++
Представленный ниже код не компилируется. В чем здесь может быть проблема? class CFirst { public: int GetValue() { return...

Реализация полиморфизма - C++
Читал что существует примерно 10 способов реализации полиморфного контейнера. Видел только один где создается виртуальный класс и у него...

иллюстрация полиморфизма - C++
Доброго времени суток!написал примитив для иллюстрации полиморфизма,ориентировался по видеокурсам с ТыТрубы #include&lt;iostream.h&gt; ...

Использование полиморфизма - C++
Помогите написать программу, которая использует перегрузительную функцию для работы с данными типов long и double и определяет...

Виды полиморфизма C++ - C++
Разбираю полиморфизм. Наткнулся на классификацию с тремя видами:1.специальный, 2.параметрический и 3.подтипов(включения). Все ли...

В чем смысл полиморфизма - C++
Объясните, пожалуйста, смысл полиморфизма. Не могу никак вникнуть. Где и как он используется? И примерчик, пожалуйста

100
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 13:10 #46
Цитата Сообщение от taras atavin Посмотреть сообщение
джавовики умудряются вообще отличать объекты от скаляров
А в С++ объект это область памяти. Скаляр тоже "область памяти". Для скаляров можно вызывать конструктор с параметрами. В ООП делается всё возможное, чтобы встроенные типы считались объектами.
0
taras atavin
3570 / 1753 / 91
Регистрация: 24.11.2009
Сообщений: 27,619
23.11.2011, 13:18 #47
Цитата Сообщение от Deviaphan Посмотреть сообщение
"тип int*" и "указатель на тип int" это одно и то же.
Вот именно. Но сам указатель типа int*, а не int.
C++
1
int *p;
Указатель здесь зовут p и это указатель на int. А теперь забыли слово "указатель". Какого типа p? В данном случае p типа int*. И никакого двойного! "p типа int*" - это
C++
1
int *p;
, различие в другом языке (русском вместо плюсов). А теперь вспоминаем, что p - это указатель. Так какого типа указатель? Конечно же типа double+! Нет? Неужели типа float/? Опять нет? Может типа char-?

Добавлено через 36 секунд
Цитата Сообщение от Deviaphan Посмотреть сообщение
джавовики умудряются вообще отличать объекты от скаляров
то есть не отличать.

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

Добавлено через 49 секунд
Цитата Сообщение от Deviaphan Посмотреть сообщение
В ООП делается всё возможное, чтобы встроенные типы считались объектами.
Делается, но плюсодевелоперами до конца не сделано.
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
23.11.2011, 13:25 #48
Цитата Сообщение от taras atavin Посмотреть сообщение
Ну ка вызови на плюсах.
Вот, конструктор с параметром
C++
1
int a(123);
Про методы я ничего не говорил. Если проигнорировать вызов operator+ как метода, то можно сделать объект, внешне идентичный скаляру.

Впрочем беседа уже свелась к тому, называть фигуру "квадрат" или "равносторонний прямоугольник"...

Добавлено через 28 секунд
Цитата Сообщение от Deviaphan Посмотреть сообщение
int a(123);
или
C++
1
double b = double(3.14);
0
Bers
Заблокирован
24.11.2011, 09:00  [ТС] #49
Цитата Сообщение от Deviaphan Посмотреть сообщение
Слово "указатель" семантично равнозначно добавлению * к названию типа. Т.е. "тип int*" и "указатель на тип int" это одно и то же.
Несколько раз перечитывал запись, пытаясь понять.
Всегда считал, что int* это и есть указатель

Сам по себе указатель - это и есть тип.

Другое дело, что переменная типа указатель содержит адрес объекта.
И эту переменную так же называют указателем.
C++
1
2
3
A* ptr = new C; //семантически, A* это объявление указателя типа A
                        //ptr - указатель типа A                        
                        //ptr - это указатель, который смотрит на объект типа C
В чем здесь подвох? Подвох в том, что "ptr указатель на A".
О том, что на самом деле он смотрит на объект типа C знают только программисты, да может быть всякие там динамик_касты.
Компилятор же считает, что указатель_на_тип априори указывает на объекты такого же типа.

Указатель_на_тип - нужно понимать, как тип самого указателя.

По сабжу, есть только два варианта:
1 Либо забить на производительность и юзать динамик_касты.
2 Либо программист сам отдаёт отчет в том, куда в каждый момент времени смотрят указатели, и какие там на самом деле живут объекты. И не допустить срезки.
Самый простой способ добиться такого эффекта - не юзать множественное наследование при полиморфизме. И вообще не делать глубоких/широких наследований. Тогда не придётся втыкать в низко-уровневую работу полиморфизма.
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 09:13 #50
Цитата Сообщение от Bers Посмотреть сообщение
Несколько раз перечитывал запись, пытаясь понять.
Всегда считал, что int* это и есть указатель
Именно об этом я и написал, да.

Добавлено через 1 минуту
Вернее - нет.)
int* это тип, а переменная этого типа - указатель.

Добавлено через 2 минуты
Цитата Сообщение от Bers Посмотреть сообщение
1 Либо забить на производительность и юзать динамик_касты.
2 Либо программист сам отдаёт отчет в том, куда в каждый момент времени смотрят указатели, и какие там на самом деле живут объекты. И не допустить срезки.
1. Да
2. Нет. Программист не должен об этом думать. Если он должен об этом думать, скорее всего это ошибка проектирования.
Срезка бывает только при использовании значений. При использовании указателей и ссылок срезки не бывает.
0
Bers
Заблокирован
24.11.2011, 09:23  [ТС] #51
Цитата Сообщение от Deviaphan Посмотреть сообщение
Срезка бывает только при использовании значений.
Указатель на ЦЕ приводится к указателю на А не для того, что бы хранить указатель на А, и перед использованием привести его обратно к указателю на ЦЕ.

по сабжу, именно поэтому и происходила срезка - использовался указатель на А, ну или на Б, без учета того, на что действительно смотрел указатель.

А как это вообще получилось? У человека была самодельная фабрика, которая производила объекты типа ЦЕ, а наружу выдавала указатели на А.
И уже снаружи, вызывающая сторона приводила А к чему ей было нужно, ничего не зная о том, какой в действительности был сконструирован объект самой фабрикой.

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

Итого: неоправданное падение производительности там, где этого можно было с лёгкостью избежать.


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

Лучше просто не связываться с такими архитектурными сооружениями следуя простому правилу:
1. Избегайте множественного наследования.
2. Избегайте глубокого наследования полиморфных интерфейсов.

А вообще, в с++ действует одно железное правило "ружья и собственной ноги". Оно гласит:

"Можно все! Если знаешь что делаешь. Если не знаешь - тогда нельзя"
0
fasked
Эксперт С++
4948 / 2528 / 180
Регистрация: 07.10.2009
Сообщений: 4,311
Записей в блоге: 1
24.11.2011, 10:39 #52
Цитата Сообщение от Bers Посмотреть сообщение
Лучше просто не связываться с такими архитектурными сооружениями следуя простому правилу:
1. Избегайте множественного наследования.
2. Избегайте глубокого наследования полиморфных интерфейсов.
Как об стенку горох. Тут осталось только одно сказать: "Вы просто не умеете их готовить"
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 10:43 #53
Цитата Сообщение от Bers Посмотреть сообщение
по сабжу, именно поэтому и происходила срезка
Там НЕТУ срезки. Перечитай приведённую тобой ссылку более внимательно. При срезке данные ТЕРЯЮТСЯ, в твоём примере ничего не теряется. "Срезанный" объект уже невозможно вернуть к исходному состоянию. Повторяю, у тебя НЕТ срезки. Срезка бывает только при использовании объектов по значению. При передаче через указатель срезки не бывает.


Цитата Сообщение от Bers Посмотреть сообщение
У человека была самодельная фабрика, которая производила объекты типа ЦЕ, а наружу выдавала указатели на А.
И уже снаружи, вызывающая сторона приводила А к чему ей было нужно, ничего не зная о том, какой в действительности был сконструирован объект самой фабрикой
У человека была ошибочно спроектирована архитектура проекта. Смысл абстрактных фабрик именно в том, чтобы не заморачиваться на счёт действительного типа объекта...


Цитата Сообщение от Bers Посмотреть сообщение
Итого: неоправданное падение производительности там, где этого можно было с лёгкостью избежать.
Именно так. Но падение производительности сперва нужно доказать.)


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

Добавлено через 56 секунд
Цитата Сообщение от Bers Посмотреть сообщение
Избегайте множественного наследования.
...в тех ситуациях, когда множественное наследование не нужно.
0
BRcr
4009 / 2298 / 155
Регистрация: 03.02.2011
Сообщений: 5,064
Записей в блоге: 10
24.11.2011, 10:53 #54
Цитата Сообщение от Bers Посмотреть сообщение
1. Избегайте множественного наследования.
2. Избегайте глубокого наследования полиморфных интерфейсов.
Bers, вам что, приз дадут, ежели вы измором возьмете споривших с вами в этой теме?
Не надо нечего и никому избегать, и правила все эти нумерованные тоже никому не нужны...
Просто всегда нужно знать, чего делаешь, и только-то; в этой теме уже предостаточно написано, чтоб разобраться и в приведении типов, и в dynamic_cast, и в наследовании, и в таблицах функций... правила эти ваши зачем?
0
Bers
Заблокирован
24.11.2011, 10:59  [ТС] #55
Цитата Сообщение от Deviaphan Посмотреть сообщение
Там НЕТУ срезки.
C++
1
((B*)ptr)->Func2(); //вывод: A::Func1 Почему?
Правильный ответ: потому что срезка объекта.

Добавлено через 2 минуты
Цитата Сообщение от Deviaphan Посмотреть сообщение
Срезка бывает только при использовании объектов по значению.
Обращение к объекту по указателю - работа с самим объектом.
Работать с объектом по указателю == работать с самим объектом по значению.
При этом, если тип указателя и тип объекта, который живет на самом деле по адресу - разные, что будит? Будит срезка, или ещё какие нибудь не очень приятные приколы

Добавлено через 1 минуту
Цитата Сообщение от BRcr Посмотреть сообщение
Bers, вам что, приз дадут, ежели вы измором возьмете споривших с вами в этой теме
Вам показалось. Я ни с кем не спорю.
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 11:02 #56
Цитата Сообщение от Bers Посмотреть сообщение
Правильный ответ: потому что срезка объекта.
Ответ не верный. Ты просто не понимаешь, что такое срезка.
Почему так происходит я уже разжевал и разложил по полочкам.
То, что ты не желаешь читать - не мои проблемы. Когда надумаешь действительно разобраться в проблеме, то перейди на третью страницу.

Добавлено через 2 минуты
Цитата Сообщение от Bers Посмотреть сообщение
При этом, если тип указателя и тип объекта, который живет на самом деле по адресу - разные, что будит? Будит срезка, или ещё какие нибудь не очень приятные приколы
Это не срезка. Срезка это потеря данных при копировании в процессе преобразования объекта дочернего класса к объекту базового. Т.е. когда копируются не все данные, а только те, которые известны в базовом классе. Так тебе понятнее? При работе по ссылке и указателю никакого копирования не происходит, соответственно и срезки быть не может в принципе.
Ты даже не можешь толком разобраться в примере, ссылку на который привёл. Там объект передаётся по значению, поэтому там и есть срезка.
0
Bers
Заблокирован
24.11.2011, 11:04  [ТС] #57
Цитата Сообщение от Deviaphan Посмотреть сообщение
Ответ не верный. Ты просто не понимаешь, что такое срезка
Что такое срезка по вашему?

По-моему, срезка - это когда большой объект приводится (интерпритируется) как более мелкий. И часть данных в итоге улетает в трубу.

В данном случае, компилятор считает что по указанному адресу живет объект типа A
Ну или объект типа B

На самом деле там живет объект типа C, но при обращении к объекту через указатель, компилятор воспринимает объект не как С, а как А или B.

Таким образом, объект, с которым происходит работа через левый указатель, оказывается срезанным.
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 11:14 #58
Цитата Сообщение от Bers Посмотреть сообщение
По-моему, срезка - это когда большой объект приводится (интерпритируется) как более мелкий.
Приводится и интерпретируется это два абсолютно разных понятия.
Более того, невозможно изменять интерпретацию объектов. Интерпретировать можно лишь указатели.
Что такое приведение объекта? Это вызов копирующего конструктора. Виртуальных конструкторов не бывает, поэтому при копировании(приведении типа) копируются не все данные. Это и есть срезка.
О том, что такое срезка я уже писал выше, поэтому повторю

Цитата Сообщение от Deviaphan Посмотреть сообщение
То, что ты не желаешь читать - не мои проблемы
Добавлено через 2 минуты
Придумал, как объяснить нагляднее!
Смотри, читай, задавай вопросы.
C++
1
2
3
4
5
6
class A;
class B : public A;
 
B b;
A * a1 = &b; // нет срезки
A a2 = b; // есть срезка
Добавлено через 3 минуты
Специально для тебя Дьюхэрст написал книгу "Скользкие места C++".
Глава по "срезке" на странице 77. Если и после этого просветление тебя не посетит, то прекращай использовать наследование в любых формах. Указателями лучше тоже кончай пользоваться.
0
Bers
Заблокирован
24.11.2011, 11:19  [ТС] #59
Цитата Сообщение от Deviaphan Посмотреть сообщение
Что такое приведение объекта? Это вызов копирующего конструктора.
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
struct Base
{
    Base(): a(333){}
    Base( const Base& istok) { std::cout << "base\n"; }
    int a;
};
 
 
struct Derrived:public Base
{
    Derrived():Base() {}
    Derrived( const Derrived& istok): Base(istok) { std::cout << "Derrived\n"; }
};
 
 
int main()
{
    
    Derrived t;
    (Base)t; //по мнению Deviaphan  здесь
                 //должен запустится копирующий 
                 //конструктор
     
    return 0;
}

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

А я вам объясню: объект это просто кучка байтов. Сама по себе не имеющая типа.
О том, как интерпритируется эта кучка байтов знает либо тип переменной, либо тип указателя.

И если тип указателя A, то компилятору пофегу, что на самом деле там живет C
При работе с объектом через такой указатель, компилятор будит считать, что там живет А

Это к вопросу об интерпретации данных.


Привести один тип к другому, не значит скопировать данные. Это значит сказать компилятору: интерпретируй этот объект, как объект другого типа.

При этом, при работе с "приведенным" типом так же может происходить срезка данных. Когда одни данные начинают восприниматься как другие данные, а часть данных вообще будит тупо проигнорирована.

Срезка происходит не когда данные не полностью скопированы, а тогда, когда компилятор уже "потерял" часть объекта.
0
Deviaphan
Делаю внезапно и красиво
Эксперт С++
1306 / 1221 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 11:29 #60
Цитата Сообщение от Bers Посмотреть сообщение
Это к вопросу об интерпретации данных.
Почему ты думаешь, что строки 20-21 вообще есть в коде?
Интерпретация данных, это reinterpret_cast, вызвать для не скалярных типов нельзя.
Приведение типов static_cast - создание копии.
Интерпретация и приведение типа это не одно и то же.
Если строка 21 отработает, то произойдёт срезка t.
0
24.11.2011, 11:29
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
24.11.2011, 11:29
Привет! Вот еще темы с ответами:

Использование свойств полиморфизма - C++
Нужна помощь. Сгенерируйте абстрактный класс типа фигура, создайте производные от него классы типа пятиугольник, прямоугольник. В классах...

Принципы наследования и полиморфизма - C++
Даны натуральное число n, действительные числа a1 a2,...,an. Если последовательность a1 ,a2 ,...,an упорядочена по неубыванию, то...

Смысл использования полиморфизма - C++
#include &lt;iostream&gt; using namespace std; class A{ public: virtual void speak() {} };

Понятия инкапсуляции, полиморфизма и наследования - C++
Всем привет. Прошу прояснить для себя 3 основных свойства парадигмы ООП инкапсуляцию, наследования и полиморфизм. Я напишу своё видение и...


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

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

КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru