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

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

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

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

23.11.2011, 01:05. Просмотров 6955. Ответов 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)
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++
Объясните, пожалуйста, смысл полиморфизма. Не могу никак вникнуть. Где и как он используется? И примерчик, пожалуйста

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 12:16 #76
Цитата Сообщение от Сыроежка Посмотреть сообщение
Вот вам элементарный пример
Я же уже сказал, что ты прав. Лучше пункт стандарта подскажи, не могу найти.(
Bers
Заблокирован
24.11.2011, 12:20  [ТС] #77
Цитата Сообщение от Deviaphan Посмотреть сообщение
У Саттера написано, что компилятор "продлевает" время жизни переменных, на которые есть константная ссылка. Но в стандарте пункт я пока не нашёл соответствующий.
Алена спп эту тему очень хорошо раскрыла в своём блоге. Она ж с этим самым агентом влияния от корпорации Зла знакома лично) И перессказывала то, что им, сотрудником мелкомягких говорил он.
Только линк на обсуждение не помню.

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

Хотя обращение по такому указателю, все равно что работа со срезанным объектом.
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 12:22 #78
Цитата Сообщение от Bers Посмотреть сообщение
Хотя обращение по такому указателю, все равно что работа со срезанным объектом.
Нет.)
Сыроежка
Заблокирован
24.11.2011, 12:28 #79
Цитата Сообщение от Deviaphan Посмотреть сообщение
Я же уже сказал, что ты прав. Лучше пункт стандарта подскажи, не могу найти.(
Надо смотреть раздел 12.2 "Temporary objects"
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 12:34 #80
Цитата Сообщение от Сыроежка Посмотреть сообщение
12.2 "Temporary objects"
12.2/5 T3

Добавлено через 4 минуты
Теперь стало понятно, как и почему работал код, который недавно обсуждали на другой ветке. Там было
C++
1
const int & a = 123;
Теперь понятнее стало.)
Bers
Заблокирован
24.11.2011, 12:41  [ТС] #81
Пример наглядно иллюстрирует, что к одному и тому же объекту можно обращаться, и вызывать его методы. И при этом компилятор будит считать что этот объект - каждый раз разный.

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
46
47
48
49
50
51
struct First
{
    First(): a(333){}
    First( const First& istok) { std::cout << "Copy First\n"; }
    void View()
    {
        std::cout << "First\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
    int a;
};
 
struct Second
{
    Second(): b(444){}
    Second( const Second& istok) { std::cout << "Copy Second\n"; }
    void View()
    {
        std::cout << "Second\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
    int b;
};
 
 
struct Test: public First, public Second 
{
    void View()
    {
        std::cout << "Test\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
};
 
int main()
{
    Test t; //доступны a, b 
    Test* ptr= &t;
    ptr->View();
 
    First* pFirst= static_cast<First*>(ptr);
    pFirst->View(); //сейчас объекту не доступен b
 
    //компилятор реально думает, что имеет дело с объектом First
    //а не Test
    std::cout<< "SIZE = " << sizeof( *pFirst ) <<std::endl;
 
    (static_cast<Second*>(ptr))->View(); //сейчас объекту не доступен a
    
    return 0;
};
Таким образом, работая с объектом через указатель, можно интерпритировать тип объекта как угодно. Что в случае с полиморфизмом может приводить к забавным последствиям.
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 12:44 #82
Цитата Сообщение от Bers Посмотреть сообщение
Таким образом
Ты используешь НЕ виртуальные функции. Т.е. вызов функции происходит по адресу, известному во время компиляции (выводится из типа указателя). Полиморфизма в этом примере нет в принципе.
Сыроежка
Заблокирован
24.11.2011, 12:51 #83
Цитата Сообщение от 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
46
47
48
49
50
51
struct First
{
    First(): a(333){}
    First( const First& istok) { std::cout << "Copy First\n"; }
    void View()
    {
        std::cout << "First\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
    int a;
};
 
struct Second
{
    Second(): b(444){}
    Second( const Second& istok) { std::cout << "Copy Second\n"; }
    void View()
    {
        std::cout << "Second\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
    int b;
};
 
 
struct Test: public First, public Second 
{
    void View()
    {
        std::cout << "Test\n"; 
        std::cout << "Size= "<< sizeof(*this)<< std::endl;
    }
};
 
int main()
{
    Test t; //доступны a, b 
    Test* ptr= &t;
    ptr->View();
 
    First* pFirst= static_cast<First*>(ptr);
    pFirst->View(); //сейчас объекту не доступен b
 
    //компилятор реально думает, что имеет дело с объектом First
    //а не Test
    std::cout<< "SIZE = " << sizeof( *pFirst ) <<std::endl;
 
    (static_cast<Second*>(ptr))->View(); //сейчас объекту не доступен a
    
    return 0;
};


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

Признаюсь, я не понял, что вы хотели продемонстрировать.
Bers
Заблокирован
24.11.2011, 12:55  [ТС] #84
Цитата Сообщение от Deviaphan Посмотреть сообщение
Ты используешь НЕ виртуальные функции. Т.е. вызов функции происходит по адресу, известному во время компиляции (выводится из типа указателя). Полиморфизма в этом примере нет в принципе.
Данный пример наглядно демонстрирует, как один и тот же объект может быть интерпретирован и так, и этак.


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

В примере по сабжу используется.
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 13:01 #85
В принципе, можно и так писать
C++
1
2
Test * t = (Test*)123456;
t->View();
Причём, этот код даже работать будет. Причём, даже правильно. Потому что данной функции безразлично состояние объекта.)

Добавлено через 1 минуту
Некоторые гвозди микроскопом забивают, но ведь это не значит, что микроскопом не нужно пользоваться. А если будете гвозди микроскопом забивать, то это "может иметь неочевидные последствия".
Bers
Заблокирован
24.11.2011, 13:06  [ТС] #86
Цитата Сообщение от Deviaphan Посмотреть сообщение
Некоторые гвозди микроскопом забивают, но ведь это не значит, что микроскопом не нужно пользоваться. А если будете гвозди микроскопом забивать, то это "может иметь неочевидные последствия".
Плоха та архитектура, которая вынуждает программиста все время помнить о низко-уровневой работе механизмов, и все время думать: а нет ли здесь каких нибудь невидимых граблей?

Одна из фундаментальных задач ООП - гасить сложность архитектуры проекта.
А не увеличивать её.

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

На мой взгляд, это плохая практика.
Гораздо выгоднее использовать неглубокие, и неширокие деревья наследований + композиция.

Вместо потенциально опасных глубоких и запутанных иерархий
taras atavin
Ушёл с форума.
3569 / 1752 / 91
Регистрация: 24.11.2009
Сообщений: 27,619
24.11.2011, 14:03 #87
Цитата Сообщение от Bers Посмотреть сообщение
Плоха та архитектура, которая вынуждает программиста все время помнить о низко-уровневой работе механизмов, и все время думать: а нет ли здесь каких нибудь невидимых граблей?
Где грабли? Решил сделать типы не совместимыми - не приводи один к другому. Плоха технология намеренного отказа от гибкости из-за якобы абстрактной опасности. Например, указатель опасен не абстрактно, а только при выдаче на чужой уровень. И необходимо его просто иметь. Использовать лучше всего только внутри классов, но при большом желании можно даже глобально. Но при работе с каким либо уровнем абстракции любая технология должна позволять ограничиваться рассмотрением только этого одного уровня. Больше того, эта возможность должна всегда и использоваться. Ну так вот, с какими бы классами ты ни работал, их отношение вида предок-потомок - именно тот уровень, с которым ты при этом работаешь. Ну так вспоминай иерархию и смотри, какой класс от кого происходит. Если классы не связаны, то нельзя приводить. И не надо кивать на динамический каст. Эта технология продвинутая, она умеет превращать даже автомобиль в лодку и назад, если уже изобретены амфибии. Нет амфибий - лодка и автомобиль не совместимы даже для этой технологии. Старая же технология приведения работает только с указателем без обращения к таблицам. И для неё лодка с автомобилем остаются несовместимыми в не зависимости от изобретения гибридного транспорта. В частности поэтому, кстати, опасны любые библиотеки классов, пока не изучишь их иерархию. Да и просто семантика методов и их параметров. Вот взять строки. С какого символа они начинаются? С первого, или с нулевого? Если класс строк не делает какой нибудь разбор сам, то тебе может понадобиться сделать его как посимвольный. Какой символ последний? Измерить длину и от неё? Если ты думаешь, что строка начинается нулевым символом, а на самом деле первым, то ты никогда не обработаешь последний символ, если в нём важная информация, то ты её никогда не учтёшь, прога просто повиснет. Даже утечка далеко не так страшна. Ну кончится память. Перезагрузи прогу и может быть ось за ней подчистит. Не помогло? Перезагрузи ось и память вся вернётся. А за висяк ты не пройдёшь никогда. И тестить бесполезно, пока не догадаешься, куда смотреть. К тому же даже если ты допустил ошибку, провоцирующую утечку, её можно найти просто внимательным чтением только своего исходника и больше ничего. Даже тест не нужен в принципе. То же самое, если важен первый символ, ты его считаешь первым, а он нулевой. Висяк слепишь и даже не увидишь. Вот попроюбуй bmp файл переименованием превратить в метафайл, а потом исполнить. А!!! Винда суперопасна!!! В ней под водой плавают грабли переименования!!! Не делай фигни - не изобретёшь грабли. Причём, они твои, а не полиморфизма. Ещё опаснее шаблон, если не понимаешь его назначения и начинаешь скармливать классы банковских счетов, или каких нибудь интерфейсов шаблону строк с отдельным полем числа символов, конкретизируемым unsigned small int для строк до 255-ти символов, unsigned short int - до 65535 и unsigned long int - до 4-х гигасимволов.
Bers
Заблокирован
24.11.2011, 17:54  [ТС] #88
Итак. Я уже почти было убедился, что приведение типов вызывает конструктор копирования.

То есть, "привести тип A к типу B" фактически означает "создать временный объект типа B, при помощи конструктора копирования этого типа B, аргументом которого будит являться объект типа А".

То бишь, временный объект типа B будит создан при помощи копирующего конструктора по прототипу типа A.


На самом деле, для меня это было открытием. Я то думал, что приведение типов меняет лишь интерпретацию типа объекта, и никаких расходов на копирование не произойдёт!

Естественно, я обеспокоился, и начал обшаривать существующую архитектуру своего прожекта.
Поскольку я пихал приведение типов везде где только можно. Я ж не знал, что это может привести к падению производительности из-за запусков копирующих конструкторов.

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

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

И я был поражон....

Объясните мне, почему так?

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
struct Base
{
    Base():a(333){}
    Base(const Base& istok): a(istok.a){ std::cout<< "Copy\n"; }
    void Print(const Base& istok )  {    std::cout<< "Print: a="<<istok.a<<std::endl;  }
    int a;
};
 
struct Derrived: public Base
{
    void Print(const Derrived& istok ) 
    { 
        std::cout<< "Derrived Print\n";
        Base::Print( istok ); //здесь происходит приведение типа
                              //от потомка к базовому
                              //почему при этом не срабатывает 
                              //копирующий конструктор?
    }
};
 
 
 
int main()
{
    Derrived test;
    test.Print(test);
    return 0;
};
Что я не так понимаю? Методу Print Derrived скармливается объект типа Derrived
А он в свою очередь проталкивает этот объект своему базовому классу.

Но базовый класс в качестве аргумента принимает только объекты своего класса!
Таким образом в момент передачи типа Derrived методу класса Base должно произойти приведение типа!

И соответственно, должен быть вызван копирующий конструктор. Почему этого не произошло?
Неужели сменилась просто интерпретация типа?
Deviaphan
Делаю внезапно и красиво
Эксперт C++
1286 / 1220 / 50
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 18:00 #89
Цитата Сообщение от Bers Посмотреть сообщение
И соответственно, должен быть вызван копирующий конструктор. Почему этого не произошло?
Не должен. Ведь ты передаёшь по ссылке. При передаче по ссылке или по указателю приведение типа объекта не происходит, происходит приведение типа ссылки. Соответственно и срезки не будет. Именно поэтому по ссылке и передают, чтобы избежать ненужного копирования.
Bers
Заблокирован
24.11.2011, 18:05  [ТС] #90
Цитата Сообщение от Deviaphan Посмотреть сообщение
Не должен. Ведь ты передаёшь по ссылке. При передаче по ссылке или по указателю приведение типа объекта не происходит, происходит приведение типа ссылки. Соответственно и срезки не будет. Именно поэтому по ссылке и передают, чтобы избежать ненужного копирования.
Хм... ну я у себя везде где ток можно юзал ссылки. А где не можно - юзал указатели.
Я то собрался код чистить, а он у меня оказывается такой... итак чистенький)

А тут значится, что ссылка - это не просто псевдоним объекта. Это вообще как бы и не объект вовсе.. гм гм... Это именно указатель на объект. Только замаскированный так, словно "другое имя объекта".
MoreAnswers
Эксперт
37091 / 29110 / 5898
Регистрация: 17.06.2006
Сообщений: 43,301
24.11.2011, 18:05
Привет! Вот еще темы с ответами:

Использование свойств полиморфизма - 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 основных свойства парадигмы ООП инкапсуляцию, наследования и полиморфизм. Я напишу своё видение и...


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

Или воспользуйтесь поиском по форуму:
Yandex
Объявления
24.11.2011, 18:05
Ответ Создать тему
Опции темы

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