Лекция 20 Преобразования типов 1 • При выполнении программы производятся явные и неявные преобразования величин из одного типа в другой. • Неявные преобразования выполняются в соответствии с правилами. • Для выполнения явных преобразований типа в С++ существует целая группа операций — const_cast, dynamic_cast, reinterpret_cast и static_cast, а также операция приведения типа, унаследованная из языка С. 2 Операция приведения типов в стиле С Операция может записываться в двух формах: тип (выражение) (тип) выражение Результатом операции является значение заданного типа, например: int a = 2; float b = 6.8; printf("%lf %d", double (a), (int) b); 3 Величина a преобразуется к типу double, а переменная b — к типу int с отсечением дробной части, в обоих случаях внутренняя форма представления результата операции преобразования иная, чем форма исходного значения. • Необходимость в преобразовании типа возникает, например, в случае, когда функция возвращает указатель на тип void, который требуется присвоить переменной конкретного типа для последующего выполнения с ней каких-либо действий: 4 • float *q = (float *) malloc(100 * sizeof(float)); • Явное преобразование типа является источником возможных ошибок, поскольку вся ответственность за его результат возлагается на программиста. • Поэтому в С++ введены операции, позволяющие выполнять частичный контроль выполняемых преобразований или сделать намерения программиста более явными для понимания. • Рассмотренное выше преобразование в стиле С оставлено в С++ только для нисходящей совместимости, и использовать его не рекомендуется. В зависимости от вида требуемого преобразования необходимо использовать соответствующую ему операцию приведения типа. 5 Операция const_cast • Операция служит для удаления модификатора const. Как правило, она используется при передаче в функцию константного указателя на место формального параметра, не имеющего модификатора const. Формат операции: • const_cast <тип> (выражение) • Обозначенный тип должен быть таким же, как и тип выражения, за исключением модификатора const. Обычно это указатель. Операция формирует результат указанного типа. 6 • Необходимость введения этой операции обусловлена тем, что программист, реализующий функцию, не обязан описывать не изменяемые в ней формальные параметры как const, хотя это и рекомендуется. • Правила C++ запрещают передачу константного указателя на место обычного. Операция const_cast введена для того, чтобы обойти это ограничение. Естественно, функция не должна пытаться изменить значение, на которое ссылается передаваемый указатель, иначе результат выполнения программы не определен. 7 • Пример: • void print(int *p){// Функция не изменяет //значение *p • cout << *p;} • const int *p; • /* print(&p); Ошибка, поскольку p объявлен как указатель на константу */ • Операция const_cast используется в том случае, когда программист уверен, что в теле функции значение, на которое ссылается указатель, не изменяется. Естественно, если есть возможность добавить к описанию формального параметра модификатор const, это предпочтительнее использования преобразования типа при вызове 8 функции. Операция static_cast • Операция static_cast используется для преобразования типа на этапе компиляции между: • целыми типами; • целыми и вещественными типами; • целыми и перечисляемыми типами; • указателями и ссылками на объекты одной иерархии, при условии, что оно однозначно и не связано с понижающим преобразованием виртуального базового класса. 9 • Формат операции: • static_cast <тип> (выражение) • Результат операции имеет указанный тип, который может быть ссылкой, указателем, арифметическим или перечисляемым типом. • При выполнении операции внутреннее представление данных может быть модифицировано, хотя численное значение остается неизменным. Например: • float f = 100; • int i = static_cast <int> (f); // Целые и вещественные имеют различное внутреннее представление 10 • Такого рода преобразования применяются обычно для подавления сообщений компилятора о возможной потере данных в том случае, когда есть уверенность, что требуется выполнить именно это действие. Результат преобразования остается на совести программиста. • Операция static_cast позволяет выполнять преобразования из производного класса в базовый и наоборот без ограничений: • class B{}; • class C: public B{}; • C c; • B *bp = static_cast<B*>(c); // Производный -> //базовый • B b; • C &cp = static_cast<C&>(b); // Базовый -> //производный 11 • Преобразование выполняется при компиляции, при этом объекты могут не быть полиморфными. Программист должен сам отслеживать допустимость дальнейших действий с преобразованными величинами. • В общем случае использование для преобразования указателей родственных классов иерархии предпочтительнее использовать операцию dynamic_cast. В этом случае если преобразование возможно на этапе компиляции, генерируется тот же код, что и для static_cast. • Кроме того, dynamic_cast допускает перекрестное преобразование, нисходящее приведение виртуального базового класса и производит проверку допустимости приведения во время выполнения. 12 Операция reinterpret_cast • Операция reinterpret_cast применяется для преобразования не связанных между собой типов, например, указателей в целые или наоборот, а также указателей типа void* в конкретный тип. При этом внутреннее представление данных остается неизменным, а изменяется только точка зрения компилятора на данные. • Формат операции: • reinterpret_cast <тип> (выражение) • Результат операции имеет указанный тип, который может быть ссылкой, указателем, целым или вещественным типом. 13 • Пример: char *p = reinterpret_cast <char*>(malloc(100)); long l = reinterpret_cast <long>(p); • Различие между static_cast и reinterpret_cast позволяет компилятору производить минимальную проверку при использовании static_cast, а программисту — обозначать опасные преобразования с помощью reinterpret_cast. • Результат преобразования остается на совести программиста. 14 Операция dynamic_cast • Операция применяется для преобразования указателей родственных классов иерархии, в основном — указателя базового класса в указатель на производный класс, при этом во время выполнения программы производится проверка допустимости преобразования. • Формат операции: • dynamic_cast <тип *> (выражение) 15 • Выражение должно быть указателем или ссылкой на класс, тип — базовым или производным для этого класса. После проверки допустимости преобразования в случае успешного выполнения операция формирует результат заданного типа, в противном случае для указателя результат равен нулю (Если выражение равно нулю, результат также равен нулю), а для ссылки порождается исключение bad_cast. Если заданный тип и тип выражения не относятся к одной иерархии, преобразование не допускается. 16 • Преобразование из базового класса в производный называют понижающим (downcast), так как графически в иерархии наследования принято изображать производные классы ниже базовых. Приведение из производного класса в базовый называют повышающим (upcast), а приведение между производными классами одного базового или, наоборот, между базовыми классами одного производного — перекрестным (crosscast). 17 Повышающее преобразование • Выполнение с помощью операции dynamic_cast повышающего преобразования равносильно простому присваиванию: class B{ /* */ }; class C: public B{ /* */ }; C* c = new C; B* b = dynamic_cast<B*>(c); // Эквивалентно B* b = с; 18 Понижающее преобразование • Чаще всего операция dynamic_cast применяется при понижающем преобразовании — когда компилятор не имеет возможности проверить правильность приведения. • Производные классы могут содержать функции, которых нет в базовых классах. Для их вызова через указатель базового класса нужно иметь уверенность, что этот указатель в действительности ссылается на объект производного класса. Такая проверка производится в момент выполнения приведения типа с использованием RTTI (run-time type information) — «информации о типе во время выполнения программы». Для того чтобы проверка допустимости могла быть выполнена, аргумент операции dynamic_cast должен быть полиморфного типа, то есть иметь хотя бы один виртуальный метод. 19 • Для полиморфного объекта реализация операции dynamic_cast весьма эффективна, поскольку ссылка на информацию о типе объекта заносится в таблицу виртуальных методов, и доступ к ней осуществляется легко. • С точки зрения логики требование, чтобы объект был полиморфным, также оправдано: ведь если класс не имеет виртуальных методов, его нельзя безопасным образом использовать, не зная точный тип указателя. А если тип известен, использовать операцию dynamic_cast нет необходимости. 20 • Результат применения операции dynamic_cast к указателю всегда требуется проверять явным образом. В приведенном ниже примере описан полиморфный базовый класс B и производный от него класс C, в котором определена функция f2. Для того чтобы вызывать ее из функции demo только в случае, когда последней передается указатель на объект производного класса, используется операция dynamic_cast с проверкой результата преобразования: 21 #include <iostream.h> #include <typeinfo.h> class B{ public: virtual void f1(){}; }; class C: public B{ public: void f2(){cout << "f2";}; }; Для использования RTTI необходимо подключить к программе заголовочный файл <typeinfo>. Кроме того, необходимо, чтобы был установлен соответствующий режим компилятора. 22 void demo(B* p){ C* c = dynamic_cast<C*>(p); if (c) c->f2(); else cout << "Передан не класс С"; } int main(){ B* b = new B; demo(b); // Выдается сообщение "Передан не класс С" C* c = new C; demo(c); // Выдается сообщение "f2" (правильно) return 0; } 23 • При использовании в этом примере вместо dynamic_cast приведения типов в стиле С, например: • C* c = (C*) p; • проконтролировать допустимость операции невозможно, и если указатель p на самом деле не ссылается на объект класса C, это приведет к ошибке. • Другим недостатком приведения в стиле С является невозможность преобразования в производный виртуального базового класса, это запрещено синтаксически. С помощью операции dynamic_cast такое преобразование возможно при условии, что класс является полиморфным и преобразование недвусмысленно. 24 • Рассмотрим пример, в котором выполняется понижающее преобразование виртуального базового класса: • • • • • • • #include <iostream.h> #include <typeinfo.h> class A{ public: virtual ~A(){};}; class B: public virtual A{}; class C: public virtual A{}; class D: public B, public C{}; // // A / \ // B C // \ / // D 25 void demo(A *a){ D* d = dynamic_cast<D*>(a); if (d) { } } int main(){ D *d = new D; demo(d); return 0; } 26 Преобразование ссылок • Для аргумента-ссылки смысл операции преобразования несколько иной, чем для указателя. Поскольку ссылка всегда указывает на конкретный объект, операция dynamic_cast должна выполнять преобразование именно к типу этого объекта. Корректность приведения проверяется автоматически, в случае несовпадения порождается исключение bad_cast: 27 #include <iostream.h> #include <typeinfo.h> class B{ public: virtual void f1(){}; }; class C: public B{ public: void f2(){ }; }; 28 void demo(B& p){ try{ C& c = dynamic_cast<C&>(p); c.f2(); } catch(bad_cast){ } } int main(){ B* b = new B; demo(*b); C* c = new C; demo(*c); return 0; // Порождается исключение // Правильно } 29 Перекрестное преобразование • Операция dynamic_cast позволяет выполнять безопасное преобразование типа между производными классами одного базового класса, например: #include <iostream.h> #include <typeinfo.h> class B{ public: virtual void f1(){}; }; class C: public B{ public: void f2(){ }; }; 30 class D: public B{ }; void demo(D* p){ C* c = dynamic_cast<C*>(p); if(c)c->f2(); else cout <<" не выполнено "; } int main(){ B* b = new C; demo((D*)b); return 0; } Классы C и D являются производными от класса B. Функции demo передается указатель на класс D, являющийся на самом деле указателем на «братский» для него класс С, поэтому динамическое преобразование типа из D в C в31 функции demo завершается успешно. • При необходимости можно осуществить преобразование между базовыми классами одного производного класса, например: #include <iostream.h> #include <typeinfo.h> class B{ public: virtual void f1(){ }; // B C }; // \ / class C{ // D public: virtual void f2(){ }; }; 32 class D: public B, public C{}; void demo(B* b){ C* c = dynamic_cast<C*>(b); if(c)c->f2(); } int main(){ D* d = new D; demo(d); return 0; } 33 • Класс D является потомком B и C, поэтому содержит методы обоих классов. Если в функцию demo передается на самом деле указатель не на B, а на D, его можно преобразовать к его второму базовому классу C. 34 Динамическое определение типа • Механизм идентификации типа во время выполнения программы (RTTI) позволяет определять, на какой тип в текущий момент времени ссылается указатель, а также сравнивать типы объектов. Для доступа к RTTI в стандарт языка введена операция typeid и класс type_info. • Формат операции typeid: • typeid (тип) • typeid (выражение) 35 • Операция принимает в качестве параметра имя типа или выражение и возвращает ссылку на объект класса type_info, содержащий информацию о типе. Если операция не может определить тип операнда, порождается исключение bad_typeid. • Когда операнд представляет собой указатель или ссылку на полиморфный тип, результатом является динамическая информация о типе (то есть объект type_info содержит информацию о типе объекта, на который в данный момент ссылается указатель). 36 • Если операндом является выражение, представляющее собой ссылку на неполиморфный тип, объект type_info содержит информацию о типе выражения, а не о типе объекта, на который оно ссылается. • Операцию typeid можно использовать как с основными, так и с производными типами данных. • Класс type_info описан в заголовочном файле <typeinfo> следующим образом: 37 class type_info{ public: virtual ~type_info(); bool operator==(const type_info& rhs) const; bool operator!=(const type_info& rhs) const; bool before(const type_info& rhs) const; const char* name() const; private: type_info(const type_info& rhs); type_info& operator=(const type_info& rhs); }; 38 • Метод name возвращает указатель на строку, представляющую имя типа, описываемого объектом типа type_info. • Виртуальный деструктор делает класс type_info полиморфным. • Конструктор копирования и операция присваивания объявлены как private, чтобы исключить возможность случайного копирования и присваивания объектов класса. • Операции == и != позволяют сравнивать два объекта на равенство и неравенство, а функция before выполняет побуквенное сравнение имен двух типов. Для сравнения используется конструкция вида: 39 • typeid(T1).before(typeid(T2)) • Если имя типа Т1 лексикографически предшествует имени Т2, результат будет истинным. • Точная информация о типе объекта во время выполнения программы может потребоваться, например, когда программист расширяет функциональность некоторого библиотечного базового класса с помощью производного, и невозможно или бессмысленно добавлять к базовому классу виртуальные функции. Например: #include <typeinfo.h> class B{ public: virtual ~B(){}; }; 40 class C: public B{ public: virtual void some_method(){ }; }; void demo(B* p){ if (typeid(*p) == typeid(C)) dynamic_cast<C*>(p)->some_method(); } int main(){ C* c = new C; demo(c); return 0; } 41 • Информацию о типе полезно использовать и в диагностических целях: void print_type(some_obj *p){ cout << typeid(*p).name(); } • Операция typeid не должна применяться вместо виртуальных функций и в случаях, когда тип объекта можно определить на этапе компиляции. 42