понедельник, Август 22, 2005

Приведение типов в C++

Лучшая практика по приведению типов: не делать этого. Потому что, если в программе потребовалось приведение типов, значит в этой программе с большой долей вероятности что-то неладно. Для довольно редких ситуаций, когда это все-таки действительно нужно, есть четыре способа приведения типов. Старый, оставшийся со времен C, но все еще работающий, лучше не использовать вовсе. Хотя бы потому, что конструкцию вида (Тип) очень сложно обнаружить при чтении кода программы.

const_cast
Самое простое приведение типов. Убирает так называемые cv спецификаторы (cv qualifiers), то есть const и volatile. volatile встречается не очень часто, так что более известно как приведение типов, предназначенное для убирания const. Если приведение типов не удалось, выдается ошибка на этапе компиляции.
При использовании остальных приведений типов cv спецификаторы останутся как были.

const char *str = "hello";
char *str1 = const_cast<char*>(str);

Updated 20.07.2008
Пример несколько неудачен. Если снимать const с переменной, которая изначально была const, то дальнейшее её использование приведёт к undefined behaviour. Вот хороший пример:
int i;
const int * pi = &i;
// *pi имеет тип const int,
// но pi указывает на int, который константным не является
int* j = const_cast<int *> (pi);


static_cast
Может быть использован для приведения одного типа к другому. Если это встроенные типы, то будут использованы встроенные в C++ правила их приведения. Если это типы, определенные программистом, то будут использованы правила приведения, определенные программистом.
static_cast между указателями корректно, только если один из указателей - это указатель на void или если это приведение между объектами классов, где один класс является наследником другого. То есть для приведения к какому-либо типу от void*, который возвращает malloc, следует использовать static_cast.
int * p = static_cast<int*>(malloc(100));
Если приведение не удалось, возникнет ошибка на этапе компиляции. Однако, если это приведение между указателями на объекты классов вниз по иерархии и оно не удалось, результат операции undefined. То есть, возможно такое приведение: static_cast<Derived*>(pBase), даже если pBase не указывает на Derived, но программа при этом будет вести себя странно.

dynamic_cast
Безопасное приведение по иерархии наследования, в том числе и для виртуального наследования.
dynamic_cast<derv_class *>(base_class_ptr_expr)
Используется RTTI (Runtime Type Information), чтобы привести один указатель на объект класса к другому указателю на объект класса. Классы должны быть полиморфными, то есть в базовом классе должна быть хотя бы одна виртуальная функция. Если эти условие не соблюдено, ошибка возникнет на этапе компиляции. Если приведение невозможно, то об этом станет ясно только на этапе выполнения программы и будет возвращен NULL.
dynamic_cast<derv_class &>(base_class_ref_expr)
Работа со ссылками происходит почти как с указателями, но в случае ошибки во время исполнения будет выброшено исключение bad_cast.

reinterpret_cast
Самое нахальное приведение типов. Не портируемо, результат может быть некорректным, никаких проверок не делается. Считается, что вы лучше компилятора знаете как на самом деле обстоят дела, а он тихо подчиняется. Не может быть приведено одно значение к другому значению. Обычно используется, чтобы привести указатель к указателю, указатель к целому, целое к указателю. Умеет также работать со ссылками.

reinterpret_cast<whatever *>(some *)
reinterpret_cast<integer_expression>(some *)
reinterpret_cast<whatever *>(integer_expression)

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


Что делает приведение типов в стиле С: пытается использовать static_cast, если не получается, использует reinterpret_cast. Далее, если нужно, использует const_cast .

Примеры
unsigned* и int* никак не связаны между собой. Есть правило приведения между unsigned (int) и int, но не между указателями на них. И привести их с помощью static_cast не получится, придется использовать reinterpret_cast. То есть вот так работать не будет:
unsigned* v_ptr;
cout << *static_cast<int*>(v_ptr) <<endl;


Приведение вниз по иерархии:
class Base { public: virtual ~Base(void) { } };
class Derived1 : public Base { };
class Derived2 : public Base { };
class Unrelated { };

Base* pD1 = new Derived1;
Вот такое приведение корректно: dynamic_cast<Derived1 *>(pD1);
А вот такое возвратит NULL: dynamic_cast<Derived2 *>(pD1);
Никак не связанные указатели можно приводить с помощью reinterpret_cast:
Derived1  derived1;
Unrelated* pUnrelated = reinterpret_cast<Unrelated*>(&derived1);

Пример использования static_cast:
int*   pi;
void* vp = pi;
char* pch = static_cast<char*>(vp);

Примеры использования reinterpret_cast:
float f (float);
struct S {
float x;
float f (float);
} s;
void g () {
reinterpret_cast<int *>(&s.x);
reinterpret_cast<void (*) ()>(&f);
reinterpret_cast<int S::*>(&S::x);
reinterpret_cast<void (S::*) ()>(&S::f);
reinterpret_cast<void**>(reinterpret_cast<long>(f));
}

Приведение в стиле C можно использовать, чтобы избавиться от значения, возвращаемого функцией. Польза от этого сомнительная, правда...
string sHello("Hello");
(void)sHello.size(); // Throw away function return
Также я видела использование приведение типов в стиле С для приведения к приватному базовому классу, но для этого можно использовать и reinterpret_cast.

Ссылки по теме:
1996 С++ Drawft Standarts - Expressions
compl.lang.c++.moderated. static_cast vs. reinterpret_cast
borland.public.cppbuilder.vcl.components.using. static_cast, dynamic_cast, reinterpret_cast
compl.lang.c++.moderated. static cast
compl.lang.c++.moderated. New-Style Casts

Technorati tag:

24 коммент.:

TheBits комментирует...

А зачем Вы пишите это в своём блоге? Памятка для самого себя или для других.

PS: Я читаю. Очень классно. Получше чем у нашего преподавателя…

Alena комментирует...

Поскольку этот вопрос мне задается уже не в первый раз, ответила отдельным постом.

TheBits комментирует...

Спасибо.

Рома комментирует...

Вроде всё написано, но вопросов больше чем ответов. К тому же встречаются неточности.

const char *str = "hello";
char *str1 = const_cast<char>(str);

Для начала надо бы

const char * str = "hello";
char * str1 = const_cast<char *>(str);

Аналогично забыты звёздочки ещё в куче мест.

Кроме того так делать категорически нельзя. Память под строковые константы будет скорее всего доступна только на чтение и при попытке записи вылетит очень некрасивое исключение.

После фразы "Умеет также работать со ссылками" видим работу с указателями :)

reinterpret_cast<whatever>(some *)

reinterpret_cast<integer_expression>(some *)

Итого: За идёю 5, за реализацию 3.

Alena комментирует...

Аналогично забыты звёздочки ещё в куче мест.
И не только звездочки. Вернула все на место.

Alena комментирует...

Кроме того так делать категорически нельзя. Память под строковые константы будет скорее всего доступна только на чтение и при попытке записи вылетит очень некрасивое исключение.
Даже если работать не со строкой, запись в реально константный объект даст undefined behavior, насколько я помню. Но можно снять const, чтобы передать переменную в функцию из не const-корректной библиотеки. При условии, что точно знаешь, что внутри этой самой функции писать никто ничего не будет. Это не самое хорошее решение, но в принципе так сделать можно.

Roma комментирует...

Тут какое дело. То что Undefined будет в любом случае это ясно.

Но если в случае объектов этот номер ещё может пройти, то в случае константных строк они могут (зависит от компилятора и процессора) располагаться в сегменте памяти защищённом от записи на аппаратном уровне.

Кстати, раз уж затронули константность, неплохо бы про mutable написать :)

Alena комментирует...

Кстати, раз уж затронули константность, неплохо бы про mutable написать :)

Дык уже: mutable и const_cast

Alexey комментирует...

Есть ещё интересное свойство reinterpret_cast, которое осталось в тени. Нельзя приводить указатель на функцию к указателю на объект:

int foo ();
reinterpret_cast < void* > (foo); //ошибка

По поводу преобразования к (void) - это единственно C-style преобразование, аналога которому нет в C++. Эту операцию можно применять для борьбы с предупреждением о неиспользуемой переменной:

void foo (int i) {
assert (i > 2);
(void) i; // без этой строчки будет warning в оптимизированной версии
}

Di комментирует...

Боже, это прекрасно, что вы пишете про ЭТО. Конкретно у Вас узнал про "reinterpret_cast" - страшная штука.)

Анонимный комментирует...

int var = 7;

const int * p3 = new int ( var );
*const_cast < int * > ( p3 ) = 3;
std::cout << *p3 << std::endl;
delete p3; p3 = NULL;

Этот код выполняется нормально. Освобождаем память, присваиваем указателю NULL.

А как сделать к примеру с константой-указателем?

int * const p4 = new int ( var );
delete p4;
const_cast < int* > ( p4 ) = NULL; // <---- Error: lvalue required as left operand of assignment

Алёна комментирует...

2Анонимный:
А как сделать к примеру с константой-указателем?

Судя по вопросу, ты скомпилял эти два примера одним компилятором, после чего сделал вывод, что есть разница в приведении типов между const int * p3 и int * const p4.
На самом деле ситуация такая. Результат const_cast для указателей не является lvalue и не может стоять слева в операции присваивания (см. Стандарт 5.2.11 ). Некоторые компиляторы в некоторых случаях это пропускают, но они не правы. Твой компилятор совершенно справедливо ругается на const_cast < int* > ( p4 ) = NULL.
Можно сделать так:
int * p = const_cast < int* > ( p4 ); p = NULL;

Можно сделать так:
const_cast < int*& > ( p4 ) = NULL;

Да, поскольку все переменные изначально определены как const, то результат всех этих действий - undefined behavior. Поскольку ты убираешь const с объекта, который изначально являлся константным, а потом делаешь ему присваивание.

Анонимный комментирует...

2Алёна:

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

Mog в пальто комментирует...

А мы вышему блогу к экзамену готовимся :)
Спасибо

Анонимный комментирует...

Cпасибо класный блог, а есть вариант по STL(Контейнерам)

Алёна комментирует...

2Анонимный:
Cпасибо класный блог, а есть вариант по STL(Контейнерам)

Ну про вариант не скажу, но кое-что про контейнеры я писала. Поройтесь в постах с тегом cpp.

Анонимный комментирует...

Спасибо большое, некоторые моменты стали намного понятнее.
Почитал комментарии, и понял, что надо ещё во многом разбираться. За то тоже спасибо =)

Анонимный комментирует...

А вот такой вопрос! Как самому определить правило для приведения объектов классов?

Алёна комментирует...

2Анонимный:
А вот такой вопрос! Как самому определить правило для приведения объектов классов

Я так поняла, нужно вот это: Overloading typecasts

AndreyPutilov комментирует...

Здравствуйте
Повторю за ост участниками спасибо
Вы начали статью с того, что приведения типов впринципе надо избегать. А как вывести std::string в консоль?

Алёна комментирует...

>А как вывести std::string в консоль?

std::string MyString;

...

cout << MyString;

AP комментирует...

binary '<<' : no operator found which takes a right-hand operand of type 'std::string' (or there is no acceptable conversion)

Возможно, cout << MyString.data();
К простому char* std::string не приводится

Алёна комментирует...

2AP:

binary '<<' : no operator found which takes a right-hand operand of type 'std::string' (or there is no acceptable conversion)

Эммм.. #include <iostream> есть? Судя по сообщению об ошибке, << трактуется как бинарный сдвиг.

Я код проверила, прежде чем сюда постить. :-)

К простому char* std::string не приводится

Приводится к const char*, MyString.c_str(). Но оно тут не нужно.

NightFox комментирует...

Alena, спасибо вам огромное!