Кастуем

Введение

Используя язык со статической типизацией ( к которому относится Си++ и Си) приходится переводить значение из одного типа в другой.

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

Преобразования бывают двух типов:

  • изменяющие внутреннее представление величин (с потерей точности или без потери точности)
  • изменяющие только интерпретацию внутреннего представления.

К первому типу относится, например, преобразование целого числа в вещественное (без потери точности) и наоборот (возможно, с потерей точности) [пример], ко второму - преобразование знакового целого в беззнаковое.

Неявные преобразования:

Осуществляются когда в коде встречаются операции над разнотипными данными. В любом случае в выражениях, где участвуют разные типы, величины меньших типов, для осуществления операций преобразуются в следующие бОльшие типы. Но, т.к. арифметические операторы не принимают в качестве аргументов типы, меньше int, то можно говорить, что типы char, signed char, unsigned char, short int и unsigned short int преобразуются в тип int, если он может представить все значения (операндов выражения), или в unsigned int в противном случае. После этого операнды преобразуются к типу наиболее длинного из них, и он используется как тип результата. Полный список правил арифметического преобразования типов (взятый из Павловской), но здесь описание таких преобразований мне нравится больше.

Иногда требуется производить явные преобразования (т.е. указывать действияпо преобразованию самому, не отдавая это компилятору).
В Си++ для явного преобразования существуют как свои (рекомендованные средства), так и унаследованные из Си.

Си стиль.

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

Например:

  1. int a=2;
  2. float b=6.8;
  3. printf ( "%lf %d \n", (double)a, (int)b );

double res = (double) (13 / 7); - с таким явным преобразованием, гарантируется, что вне зависимости от реализиации компилятора в double не будет занесен результат целочисленного деления (т.к. делятся int типы)

Но это искусственный пример, более того, есть некоторое правило - если в программе есть явное преобразование - то возможно архитектура программы составлена не оптимально. Но иногда бывает, что без преобразования типов не обойтись. Один из таких случаев - выделение памяти для динамических объектов в Си, и в Си++, хотя это и спрятано в рутину операций new и new[ ].
float *q = ( float *) malloc ( 100 * sizeof(float) );

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

Возможности Си++

в C++ введены операции, позволяющие выполнять частичный контроль выполняемых преобразований. Так же конструкции в Си++ стиле более видны в коде, что возможно будет полезно при отладке и анализе программы.

const_cast

Формат операции const_cast <тип> (выражение)
Удаляет модификатор const. Как правило, она используется при передаче в функцию константного указателя на место формального параметра, не имеющего модификатора const. Так же удаляет модификатор volatile [который запрещает компилятору оптимизировать в машинном коде всю работу с данной переменной, как правило служит это для того, чтобы в область памяти данной переменной имелся доступ не только из запущенной программы, но есть и другие способы применения]. Необходимость введения этой операции обусловлена тем, что программист, реализующий функцию, не обязан описывать не изменяемые в ней формальные параметры как const, хотя это и рекомендуется. Правила C++ запрещают передачу указателя на константу на место указателя на обычную переменную. Неконстантным становится возвращаемое значение операции const_cast, а не сама переменная.

Пример 1:

  1. void print( int *р )
  2. {
  3. cout << *р;
  4. }
  5. const int *р;
  6.  
  7. print(p); //FIXME// Ошибка, поскольку р объявлен как указатель на константу
  8. print(const_cast<int*>(p)); //Нормально

dynamic_cast

Проверка возможности преобразования выполняется как на стадии компиляции так и в момент выполнения программы (dynamic)
Операция применяется для преобразования указателей родственных классов иерархии (полиморфных объектов - если объект не полиморфный - то будет ошибка компиляции), в основном — указателя базового класса в указатель на производный класс.
формат вызова dynamic_cast <тип *> (выражение) или dynamic_cast <тип &> (выражение) выражение должно быть указателем или ссылкой на класс, тип — базовым или производным для этого класса. После проверки допустимости преобразования в случае успешного выполнения операция формирует результат заданного типа, в противном случае для указателя результат равен нyлю* а для ссылки порождается исключение bad_cast (из заголовочного файла <typeinfo>). Если заданный тип и тип выражения не относятся к одной иерархии, преобразование не допускается.

Преобразование из базового класса в производный называют понижающим (downcast), так как графически в иерархии наследования принято изображать производные классы ниже базовых. Приведение из производного класса в базовый называют повышающим (upcast), а приведение между производными классами одного базового или, наоборот, между базовыми классами одного производного — перекрестным (crosscast).

Варианты преобразований dynamic_cast

Повышающее преобразование - равносильно обыкновенному присваиванию указателя на объект производного класса к указателю базового класса, поэтому отдельного рассмотрения не стоит.
Понижающее преобразование
Чаще всего операция dynamic_cast применяется при понижающем преобразовании — когда компилятор не имеет возможности проверить правильность приведения. Производные классы могут содержать функции, которых нет в базовых классах. Для их вызова через указатель базового класса нужно иметь уверенность, что этот указатель в действительности ссылается на объект производного класса. Такая
проверка производится в момент выполнения приведения типа с использованием RTTI (run-time type information) — «информации о типе во время выполнения программы». Для того чтобы проверка допустимости могла быть выполнена, аргумент операции dynamic_cast должен быть полиморфного типа, то есть иметь хотя бы один виртуальный метод.

Для полиморфного объекта реализация операции dynamic_cast весьма эффективна, поскольку ссылка на информацию о типе объекта заносится в таблицу виртуальных методов, и доступ к ней осуществляется легко. С точки зрения логики требование, чтобы объект был полиморфным, также оправдано: ведь если класс не имеет виртуальных методов, его нельзя безопасным образом использовать, не зная точный тип указателя. А если тип известен, использовать операцию dynamic_cast нет необходимости.

Результат примепения операции dynamic_cast к указателю или ссылке всегда требуется проверять явным образом. В приведенном ниже примере описан полиморфный базовый класс В и производный от него класс С, в котором определена функция f2. Для того чтобы вызывать ее из функции demo только в случае, когда последней передается указатель на объект производного класса, используется операция dynamic_cast с проверкой результата преобразования:

  1. #include <iostream>
  2. #include <typeinfo>
  3. class B{
  4. public: virtual void f1 ( ) { };
  5. };
  6.  
  7. class C: public B
  8. {
  9. public: void f2(){cout << "f2";}
  10. };
  11.  
  12. void demo( B * p )
  13. {
  14. C * с = dynamic_cast <C*> ( p ); //Для интереса - тоже но в Си стиле было\
  15.   бы так: С * с = ( С *) р;
  16. if (c) c->f2();
  17. else cout << "Передан не класс С";
  18. }
  19. int main()
  20. {
  21. В * b = new В;
  22. demo(b); // Выдается сообщение "Передан не класс С"
  23. С * с = new С;
  24. demo(c); // Выдается сообщение "f2" (правильно)
  25. return 0;
  26. }

В примере привёдет и Си способ, но при нём проконтролировать допустимость операции невозможно, и если указатель р на самом деле не ссылается на объект класса С, это приведет к ошибке. Другим недостатком приведения в стиле Си является невозможность преобразования в производный виртуального базового класса, это запрещено синтаксически. С помощью операции dynamic_cast такое преобразование возможно при условии, что класс является полиморфным. Рассмотрим пример, в котором выполняется понижающее преобразование виртуального базового класса

  1. #include <iostream>
  2. #include <typeinfo>
  3. class A
  4. {
  5. public:
  6. virtual ~A(){}; //делаем класс полиморфным
  7. //хотя бы с помощью вирт. десктр
  8. };
  9. class B: public virtual A { }; // A
  10. class C: public virtual A { }; // / \
  11. class D: public B, public C { }; // B C
  12. void demo(A *a) // \ /
  13. { // D
  14. D * d = dynamic_cast<D*>(a); //
  15. if (d) { ... }
  16. }
  17. int main ( )
  18. {
  19. D *d = new D; demo(d);
  20. return 0;
  21. }

Пример для ссылок:

  1. #include <iostream>
  2. #include <typeinfo>
  3. class B{
  4. public: virtual void f1 ( ) { };
  5. };
  6.  
  7. class C: public B
  8. {
  9. public: void f2(){cout << "f2";}
  10. };
  11.  
  12. void demo( B & p )
  13. {
  14. try
  15. {
  16. C& с = dynamic_cast <C&> ( p );
  17. c.f2();
  18. }
  19. catch(bad_cast)
  20. {
  21. cout << "Передан не класс С";
  22. ...
  23. }
  24. }
  25. int main()
  26. {
  27. В * b = new В;
  28. demo(*b); // Пораждается исключение
  29. С * с = new С;
  30. demo(*c); // Правильно
  31. return 0;
  32. }

Перекрестное преобразование

  1. #include <iostream>
  2. #include <typeinfo>
  3.  
  4. class B
  5. {
  6. public: virtual void f1 ( ) {};
  7. };
  8.  
  9. class C: public B
  10. {
  11. public: void f2() {}
  12. };
  13.  
  14. class D: public B { };
  15.  
  16. void demo(D * p)
  17. {
  18. C * c = dynamic_cast<C*>( dynamic_cast<B*>(p) );
  19. if ( c )
  20. c->f2 ( );
  21. else
  22. cout << " не выполнено ";
  23. }
  24.  
  25. int main ( )
  26. {
  27. B * b = new C; demo( (D*)b );
  28. //Указатель для передачи в функцию сформирован \
  29.   искусственным образом исключительно для \
  30.   демонстрации возможности динамического \
  31.   преобразования типа.
  32. return 0;
  33. }

Классы С и D являются производными от класса В. Функции demo передается указатель на класс D, являющийся на самом деле указателем на «братский» для него класс С, поэтому динамическое преобразование типа из D в С в функции demo завершается успешно.

При необходимости можно осуществить преобразование между базовыми классами одного производного класса, например:

  1. #include <iostream>
  2. #include <typeinfo>
  3.  
  4. class B{
  5. public: virtual void f1 ( ) { ... } // В С
  6. }; // \ /
  7. class C // D
  8. {
  9. public: virtual void f2 ( ) { ... }
  10. };
  11.  
  12. class D: public B, public C { ... };
  13.  
  14. void demo(B* b)
  15. { С * с = dynamic_cast < C* > ( b );
  16. if ( c ) c->f2( );
  17. }
  18.  
  19. int main ( )
  20. {
  21. D * d = new D; demo(d);
  22. }

Класс D является потомком В и С, поэтому содержит методы обоих классов. Если в функцию demo передается на самом деле указатель не на В, а на D, его можно преобразовать к его второму базовому классу С.

static_cast

Формат операции static_cast <тип> (выражение)
В отличии от dynamic_cast проверка возможности преобразования выполняется только во время компиляции.

static_cast применяется для преобразования между:

  • целыми типами.
  • целыми и вещественными.
  • пользовательскими типами, используя правила приведения определенные для этих типов. Причем только тех, что не состоят в иерархии, т.к. для состоящих в иерархии типов компилятор выберет свои сценарии преобразования типа, не задействуя перегруженные операторы преобразования. Так же если класс B объявляется позже класса A, то А не может быть преобразован в B, т.к. до А - B не определен. Объявление, но не определение B даст "неполный тип" (соответственно компилятор не может выбирать методы из B).
  • указателями и ссылками на объекты одной иерархии, при условии, что оно однозначно и не связано с понижающим преобразованием виртуального базового класса, т.к если понижающее преобразование удалось на этапе компиляции - static_cast<Derived*>(pBase) в коде невозмжно будет проверить (как в примерах выше) if (DerivedCL) -т.е. указывал ли pBase на Derived и программа будет вести себя странно - т.е. вроде бы должны работать методы производного класса, а они не работают.

static_cast между указателями корректно, только если один из указателей - это указатель на void или если это приведение между типами объектов одной классовой иерархии. То есть для приведения к какому-либо типу от void*, который возвращает malloc, следует использовать static_cast: int * p = static_cast<int*>(malloc(sizeof(int)*100));

Результат операции имеет указанный тип, который может быть ссылкой, указателем, арифметическим или перечисляемым типом.

При выполнении операции внутреннее представление данных может быть модифицировано, хотя численное значение остается неизменным. Например:

  1. float f = 100;
  2. int i = static_cast <int> ( f );
  3. // Целые и вещественные имеют различное внутреннее представление

Такого рода преобразования применяются обычно для подавления сообщений компилятора о возможной потере данных в том случае, когда есть уверенность, что требуется выполнить именно это действие. Результат преобразования остается на совести программиста. Операция static_cast позволяет выполнять преобразования из производного класса в базовый и наоборот без ограничений [но стоит помнить, о сказанном выше, что при понижающем преобразовании невозможно проверить результат]:

  1. class В { };
  2. class С: public В { };
  3. С * с = new C;
  4. В *= static_cast < B* > ( c ); // Производный -> базовый
  5. В b;
  6. С & ср = static_cast < C& > ( b ); // Базовый -> производный

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

Рекомеднация - везде где возможно использовать именно эту операцию преобразования, конечно кроме случаев когда нужно dynamic_cast или const _cast.

reinterpret_cast

Формат операции: reinterpret_cast <тип> (выражение)

Операция reinterpret_cast применяется для преобразования не связанных между собой типов, например, указателей в целые или наоборот, а также указателей типа void* в конкретный тип. При этом внутреннее представление данных остается неизменным, а изменяется только точка зрения компилятора на данные. Данная операция не производит никаких проверок, а просто "меняет взгляд" компилятора на тип.

Результат операции имеет указанный тип, который может быть ссылкой, указателем, целым или вещественным типом.

Примеры:

  1. /*int * */ Checksum += *(reinterpret_cast<int*>(tmp /*char * */ ));
  2. memset ( name,'\0', l_name ); int tmp; while ( ( tmp = cin.get ( ) ) != '\n' ) { strcat ( name, reinterpret_cast <char*> (&tmp) ); }

С помощью reinterpret_cast можно создать своего рода "тоннель" для вставки в ф-ю на место, например указателя на cимвол char * - автоматической int перемеменной (переданной в reinterpret_cast по адресу). В коде ниже метод ifstream:read(char* s, streamsize n) воспримет автоматическую переменную double как char*.

  1. double check_num;
  2. in.read(reinterpret_cast<char*>(&check_num), sizeof(double));

Использованный материал:

Т.А. Павловская "C/C++ Программирование на языке высокого уровня"
и
http://alenacpp.blogspot.ru/2005/08/c.html
http://cppstudio.com/post/5343/