Форум программистов, компьютерный форум, киберфорум
С++ для начинающих
Войти
Регистрация
Восстановить пароль
Карта форума Темы раздела Блоги Сообщество Поиск Заказать работу  
 
 
Рейтинг 4.96/67: Рейтинг темы: голосов - 67, средняя оценка - 4.96
Заблокирован
1

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

23.11.2011, 01:05. Показов 13181. Ответов 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
Заблокирован
24.11.2011, 12:41  [ТС] 81
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
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;
};
Таким образом, работая с объектом через указатель, можно интерпритировать тип объекта как угодно. Что в случае с полиморфизмом может приводить к забавным последствиям.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 12:44 82
Цитата Сообщение от Bers Посмотреть сообщение
Таким образом
Ты используешь НЕ виртуальные функции. Т.е. вызов функции происходит по адресу, известному во время компиляции (выводится из типа указателя). Полиморфизма в этом примере нет в принципе.
0
Заблокирован
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;
};


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

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


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

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

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

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

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

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

Вместо потенциально опасных глубоких и запутанных иерархий
0
4226 / 1795 / 211
Регистрация: 24.11.2009
Сообщений: 27,562
24.11.2011, 14:03 87
Цитата Сообщение от Bers Посмотреть сообщение
Плоха та архитектура, которая вынуждает программиста все время помнить о низко-уровневой работе механизмов, и все время думать: а нет ли здесь каких нибудь невидимых граблей?
Где грабли? Решил сделать типы не совместимыми - не приводи один к другому. Плоха технология намеренного отказа от гибкости из-за якобы абстрактной опасности. Например, указатель опасен не абстрактно, а только при выдаче на чужой уровень. И необходимо его просто иметь. Использовать лучше всего только внутри классов, но при большом желании можно даже глобально. Но при работе с каким либо уровнем абстракции любая технология должна позволять ограничиваться рассмотрением только этого одного уровня. Больше того, эта возможность должна всегда и использоваться. Ну так вот, с какими бы классами ты ни работал, их отношение вида предок-потомок - именно тот уровень, с которым ты при этом работаешь. Ну так вспоминай иерархию и смотри, какой класс от кого происходит. Если классы не связаны, то нельзя приводить. И не надо кивать на динамический каст. Эта технология продвинутая, она умеет превращать даже автомобиль в лодку и назад, если уже изобретены амфибии. Нет амфибий - лодка и автомобиль не совместимы даже для этой технологии. Старая же технология приведения работает только с указателем без обращения к таблицам. И для неё лодка с автомобилем остаются несовместимыми в не зависимости от изобретения гибридного транспорта. В частности поэтому, кстати, опасны любые библиотеки классов, пока не изучишь их иерархию. Да и просто семантика методов и их параметров. Вот взять строки. С какого символа они начинаются? С первого, или с нулевого? Если класс строк не делает какой нибудь разбор сам, то тебе может понадобиться сделать его как посимвольный. Какой символ последний? Измерить длину и от неё? Если ты думаешь, что строка начинается нулевым символом, а на самом деле первым, то ты никогда не обработаешь последний символ, если в нём важная информация, то ты её никогда не учтёшь, прога просто повиснет. Даже утечка далеко не так страшна. Ну кончится память. Перезагрузи прогу и может быть ось за ней подчистит. Не помогло? Перезагрузи ось и память вся вернётся. А за висяк ты не пройдёшь никогда. И тестить бесполезно, пока не догадаешься, куда смотреть. К тому же даже если ты допустил ошибку, провоцирующую утечку, её можно найти просто внимательным чтением только своего исходника и больше ничего. Даже тест не нужен в принципе. То же самое, если важен первый символ, ты его считаешь первым, а он нулевой. Висяк слепишь и даже не увидишь. Вот попроюбуй bmp файл переименованием превратить в метафайл, а потом исполнить. А!!! Винда суперопасна!!! В ней под водой плавают грабли переименования!!! Не делай фигни - не изобретёшь грабли. Причём, они твои, а не полиморфизма. Ещё опаснее шаблон, если не понимаешь его назначения и начинаешь скармливать классы банковских счетов, или каких нибудь интерфейсов шаблону строк с отдельным полем числа символов, конкретизируемым unsigned small int для строк до 255-ти символов, unsigned short int - до 65535 и unsigned long int - до 4-х гигасимволов.
0
Заблокирован
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 должно произойти приведение типа!

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

А тут значится, что ссылка - это не просто псевдоним объекта. Это вообще как бы и не объект вовсе.. гм гм... Это именно указатель на объект. Только замаскированный так, словно "другое имя объекта".
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
24.11.2011, 19:44 91
Цитата Сообщение от Bers Посмотреть сообщение
А тут значится, что ссылка - это не просто псевдоним объекта
Именно так. Фактически, это полиморфный указатель, но без указателя. Т.е. полиморфный адрес объекта, который используется непосредственно, а не посредством указателя. Как-то так.
0
84 / 57 / 8
Регистрация: 07.08.2010
Сообщений: 185
18.12.2011, 05:03 92
Цитата Сообщение от ValeryLaptev Посмотреть сообщение
Спустимся немного вглубь.
Все указатели на уровне реализации - абсолютно одинаковы. Поэтому явное преобразование типа указателя - это просто для порядку на уровне С++. На самом деле никаких преобразований не производится. Как и было в С.
Очевидно, это не так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <cstdio>
#include <cassert>
 
class A { int a; };
class B { int b; };
class C : public A, public B { int c; };
 
int main()
{
    C obj;
    A * a = &obj;
    B * b = &obj;
    C * c = &obj;
 
    std::printf("a != b                       : a = %p, b = %p, c = %p\n", a, b, c);
    std::printf("(C *)a == (C *)b             : (C *)a = %p, (C *)b = %p\n", (C *)a, (C *)b);
    std::printf("static cast (C *)a == (C *)b : (C *)a = %p, (C *)b = %p\n", static_cast<C *>(a), static_cast<C *>(b));
 
    assert((C *) a == (C *) b);
    assert(static_cast<C *>(a) == static_cast<C *>(b));
}
При преобразовании (C*)b происходит поправка значения указателя.

А вот при наличии в классе виртуальных функциях нужно знать тип ВО ВРЕМЯ ВЫПОЛНЕНИЯ. Чтобы вызвать нужную функцию. Это довольно сложная операция. Это и есть механизм RTTI.
* RTTI не имеет отношения к виртуальным функциям.
* dynamic_cast нужен для преобразования между типами, между которыми отсутствует наследственная связь. Иначе работает простой static_cast.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
18.12.2011, 07:41 93
Цитата Сообщение от alexzak Посмотреть сообщение
между которыми отсутствует наследственная связь.
Нет. Для преобразования от родительского к дочернему static_cast не подходит, только dynamic_cast.
0
В астрале
Эксперт С++
8049 / 4806 / 655
Регистрация: 24.06.2010
Сообщений: 10,562
18.12.2011, 12:19 94
Deviaphan, Если нету множественного наследования и есть уверенность, что преобразование корректно - reinterpret_cast.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
18.12.2011, 12:31 95
Цитата Сообщение от ForEveR Посмотреть сообщение
Если нету множественного наследования и есть уверенность, что преобразование корректно - reinterpret_cast.
Я к тому, что со static_cast вообще не скомпилируется.
0
DU
1500 / 1146 / 165
Регистрация: 05.12.2011
Сообщений: 2,279
18.12.2011, 12:40 96
статик каст подойдет и это скомпилится. только результат каста может быть не таким, какой ожидается. А компилится это потому, что компилятор видит наследственную связь между базой и производным классом. При множественном наследовании это возможно со своими хитростями будет работать или не работать, но в простом случае статик_каст от родителя к потомку компилится:

C++
1
2
3
4
5
6
7
8
9
10
11
class Base {};
class Der1 : public Base {};
class Der2 : public Base {};
 
int main()
{
  Der1 d1;
  Base* pBase = &d1;
  Der2* pDer2 = static_cast<Der2*>(pBase); // сюрприз!
  return 0;
}
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
18.12.2011, 12:52 97
Цитата Сообщение от DU Посмотреть сообщение
статик каст подойдет и это скомпилится.
Честно говоря, я потрясён. Заведомо ошибочный код компилируется - это не есть хорошо.

Добавлено через 4 минуты
Впрочем, это лишний повод не использовать static_cast там, где его использовать не следует.)
0
DU
1500 / 1146 / 165
Регистрация: 05.12.2011
Сообщений: 2,279
18.12.2011, 13:01 98
Компилябельность объяснить можно тем, что не везде используют RTTI по разным причинам. Но в то же время хочется иметь возможность каститься с хоть какой-то диагностикой на этапе компиляции. Ну и как я уже сказал, поскольку компилятор видит связь между родителем и дочерним классом, он разрешает такой каст.
0
Делаю внезапно и красиво
Эксперт С++
1313 / 1228 / 72
Регистрация: 22.03.2011
Сообщений: 3,744
18.12.2011, 13:07 99
Никогда так не писал, поэтому был уверен, что компилятор будет ругаться.) MFC не в счёт.
0
В астрале
Эксперт С++
8049 / 4806 / 655
Регистрация: 24.06.2010
Сообщений: 10,562
18.12.2011, 13:20 100
Выдержки из стандарта на эту тему.

An lvalue of type “cv1 B,” where B is a class type, can be cast to type “reference to cv2 D,” where D is a class
derived (Clause 10) from B, if a valid standard conversion from “pointer to D” to “pointer to B” exists (4.10),
cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B is neither a virtual base class
of D nor a base class of a virtual base class of D. The result has type “cv2 D.” An xvalue of type “cv1 B” may
be cast to type “rvalue reference to cv2 D” with the same constraints as for an lvalue of type “cv1 B.” If the
object of type “cv1 B” is actually a subobject of an object of type D, the result refers to the enclosing object
of type D. Otherwise, the result of the cast is undefined.

C++
1
2
3
4
5
6
struct B { };
struct D : public B { };
D d;
B &br = d;
static_cast<D&>(br);
// produces lvalue to the original d object
A prvalue of type “pointer to cv1 B,” where B is a class type, can be converted to a prvalue of type “pointer
to cv2 D,” where D is a class derived (Clause 10) from B, if a valid standard conversion from “pointer to D” to
“pointer to B” exists (4.10), cv2 is the same cv-qualification as, or greater cv-qualification than, cv1, and B
is neither a virtual base class of D nor a base class of a virtual base class of D. The null pointer value (4.10)
is converted to the null pointer value of the destination type. If the prvalue of type “pointer to cv1 B” points
to a B that is actually a subobject of an object of type D, the resulting pointer points to the enclosing object
of type D. Otherwise, the result of the cast is undefined.
1
18.12.2011, 13:20
IT_Exp
Эксперт
87844 / 49110 / 22898
Регистрация: 17.06.2006
Сообщений: 92,604
18.12.2011, 13:20
Помогаю со студенческими работами здесь

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

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

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

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


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

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