Этот пост попытка кратко оформить все, что я читал или слышал из разных источников про операторы приведения типов в языке C++. Информация ориентирована в основном на тех, кто изучает C++ относительно недолго и, как мне кажется, должна помочь понять cпецифику применения данных операторов. Старожилы и гуру С++ возможно помогут дополнить или скорректировать описанную мной картину. Всех интересующихся приглашаю под кат.
Приведение типов в стиле языка C (C-style cast)
Приведение типов в стиле языка C может привести выражение любого типа к любому другому типу данных (исключение это приведение пользовательских типов по значению, если не определены правила их приведения, а также приведение вещественного типа к указателю или наоборот). К примеру, unsigned int может быть преобразован к указателю на double. Данный метод приведения типов может быть использован в языке C++. Однако, метод приведения типов в стиле языка C не делает проверки типов на совместимость, как это могут сделать static_cast и dynamic_cast на этапе компиляции и на этапе выполнения соответственно. При этом все, что умеют const_cast и reinterpret_cast данный метод приведения типов делать может.
Общий вид приведения:
(new_type)exp
, где new_type – новый тип, к которому приводим, а exp – выражение, которое приводится к новому типу.
Т.к. данный оператор не имеет зарезервированного ключевого слова (например, static_cast) найти все места приведения типов в тексте программы будет не очень удобно, если это потребуется.
#include <iostream>
//Пустые классы только
//для теста приведения
struct AAA{
};
struct BBB{
};
//Наследники BBB
struct BBB_X:BBB{
};
struct BBB_Y:BBB{
};
int main()
{
//Переменные простых типовы и указатели на переменные простых типов
int i = 5;
double d = 111.222;
char c = 'a';
int* pi = &i;
double * pd = &d;
const int* cpi = &i;
void* v = NULL;
//Объекты классов
AAA A;
BBB B;
BBB_X BX;
BBB_Y BY;
//Указатели на объекты классов
AAA* pA = &A;
BBB* pB = &B;
BBB_X* pBX = &BX;
BBB_Y* pBY = &BY;
//Приводим явно double к int
i = (int)d;
//и наоборот
d = (double)i;
//указатель на int к char
c = (char)pi;
//char к указателю на void
v = (void*)c;
//указатель на void к указателю на int
pi = (int*)v;
//Снимаем константность const int*
pi = (int *) cpi;
//Приводим указатель на объект AAA к указателю на объект BBB
//из разных иерархий
pA = (AAA*) pB;
//Приводим указатель на double к double
d = (double)pd;//Ошибка!!!
//А если наоборот?
pd = (double*)d;//Ошибка!!!
//Перемещение из одной иерархии наследования в другую
pB = (BBB*)pBX;
pBY = (BBB_Y*) pB;
return 0;
}
const_cast
Оператор приведения const_cast удаляет или добавляет квалификаторы const и volatile с исходного типа данных (простые типы, пользовательские типы, указатели, ссылки). Например, был const int, а после преобразования стал int или наоборот. Квалификаторы const и volatile называют cv-квалификаторы (cv-qualifiers). Данные квалификаторы указываются перед именами типов. Как ни трудно догадаться квалификатор const задает константность, т.е. защищает переменную от изменения. Квалификатор volatile говорит о том, что значение переменной может меняться без явного выполнения присваивания. Это обеспечивает защиту от оптимизации компилятором операций с данной переменной.
Общий вид приведения:
const_cast<new_type>(exp)
#include <iostream>
//Снятие константности
void test_func_X(const int* in1, const int& in2)
{
int *p;
//Сняли константность и записали 33
p = const_cast<int*>(in1);
*p = 33;
//Сняли константность и записали 55
const_cast<int&>(in2) = 55;
}
//Добавление константности
void test_func_Y(int* in1, int& in2)
{
const int *p;
//Добавили константность
//и пытаемся записать 33
p = const_cast<const int*>(in1);
*p = 33;//Ошибка !!!
//Добавили константность константность
//и пытаемся записалть 33
const_cast<const int&>(in2) = 55;//Ошибка!!!
}
//Снятие volatile
void test_func_Z(volatile int* in1, volatile int& in2)
{
int *p;
//Сняли volatile и записали 33
p = const_cast<int*>(in1);
*p = 33;
//Сняли volatile и записали 55
const_cast<int&>(in2) = 55;
}
//Добавление volatile
void test_func_A(int* in1, int& in2)
{
volatile int *p;
//Добавили volatile и записали 33
p = const_cast<volatile int*>(in1);
*p = 33;
//Добавили volatile и записали 55
const_cast<volatile int&>(in2) = 55;
}
int main()
{
int x=3,y=5;
std::cout<<x<<" "<<y<<std::endl;
//Снимаем константность
test_func_X(&x,y);
std::cout<<x<<" "<<y<<std::endl;
x=3;
y=5;
//Добавляем константность
test_func_Y(&x,y);//Ошибка!!!
std::cout<<x<<" "<<y<<std::endl;
//Снимаем volatile
test_func_Z(&x,y);
std::cout<<x<<" "<<y<<std::endl;
x=3;
y=5;
std::cout<<x<<" "<<y<<std::endl;
//Добавляем volatile
test_func_A(&x,y);
std::cout<<x<<" "<<y<<std::endl;
system("pause");
return 0;
}
Дополнительный пример от пользователя 5nw
#include <iostream>
using namespace std;
void f(int *x)
{
cout << __PRETTY_FUNCTION__ << endl;
}
void f(const int *x)
{
cout << __PRETTY_FUNCTION__ << endl;
}
int main()
{
int x = 5;
int *px = &x;
f(px);
f(const_cast<const int*>(px));
return 0;
}
Квалификаторы const и volatile можно удалить или добавить только с помощью оператора приведения const_cast и приведения типов в стиле языка C. Другие операторы приведения типов не влияют на квалификаторы const и volatile (reinterpret_cast, static_cast, dynamic_cast).
reinterpret_cast
Оператор приведения reinterpret_cast используется для приведения несовместимых типов. Может приводить целое число к указателю, указатель к целому числу, указатель к указателю (это же касается и ссылок). Является функционально усеченным аналогом приведения типов в стиле языка С. Отличие состоит в том, что reinterpret_cast не может снимать квалификаторы const и volatile, а также не может делать небезопасное приведение типов не через указатели, а напрямую по значению. Например, переменную типа int к переменной типа double привести при помощи reinterpret_cast нельзя.
Общий вид приведения:
reinterpret_cast<new_type>(exp)
#include <iostream>
//Пустые классы только
//для теста приведения
struct AAA{
};
struct BBB{
};
//Наследники BBB
struct BBB_X:BBB{
};
struct BBB_Y:BBB{
};
int main()
{
//Переменные простых типовы и указатели на переменные простых типов
int i = 5;
double d = 111.222;
char c = 'a';
int* pi = &i;
double * pd = &d;
const int* cpi = &i;
void* v = NULL;
//Объекты классов
AAA A;
BBB B;
BBB_X BX;
BBB_Y BY;
//Указатели на объекты классов
AAA* pA = &A;
BBB* pB = &B;
BBB_X* pBX = &BX;
BBB_Y* pBY = &BY;
//Приводим явно double к int
i = reinterpret_cast<int>(d);//Ошибка!!!
//и наоборот
/d = reinterpret_cast<int>(i);//Ошибка!!!
//указатель на int к char
c = reinterpret_cast<char>(pi);
//char к указателю на void
v = reinterpret_cast<void*>(c);
//указатель на void к указателю на int
pi = reinterpret_cast<int*>(v);
//Снимаем константность const int*
pi = reinterpret_cast<int *>(cpi);//Ошибка!!!
//Приводим указатель на объект AAA к указателю на объект BBB
//из разных иерархий
pA = reinterpret_cast<AAA*>(pB);
//Приводим указатель на double к double
d = reinterpret_cast<double>(pd);//Ошибка!!!
//А если наоборот?
pd = reinterpret_cast<double*>(d0;//Ошибка!!!
//Перемещение из одной иерархии наследования в другую
pB = reinterpret_cast<BBB*>(pBX);
pBY = reinterpret_cast<BBB_Y*>(pB);
return 0;
}
static_cast
Оператор приведения static_cast применяется для неполиморфного приведения типов на этапе компиляции программы. Отличие static_cast от приведения типов в стиле языка C состоит в том, что данный оператор приведения может отслеживать недопустимые преобразования, такие как приведение указателя к значению или наоборот (unsigned int к указателю на double не приведет), а также приведение указателей и ссылок разных типов считается корректным только, если это приведение вверх или вниз по одной иерархии наследования классов, либо это указатель на void. В случае фиксации отклонения от данных ограничений будет выдана ошибка при компиляции программы. При множественном наследовании static_cast может вернуть указатель не на исходный объект, а на его подобъект.
Общий вид приведения:
static _cast<new_type>(exp)
#include <iostream>
//Пустые классы только
//для теста приведения
struct AAA{
};
struct BBB{
};
//Наследники BBB
struct BBB_X:BBB{
};
struct BBB_Y:BBB{
};
int main()
{
//Переменные простых типовы и указатели на переменные простых типов
int i = 5;
double d = 111.222;
char c = 'a';
int* pi = &i;
double * pd = &d;
const int* cpi = &i;
void* v = NULL;
//Объекты классов
AAA A;
BBB B;
BBB_X BX;
BBB_Y BY;
//Указатели на объекты классов
AAA* pA = &A;
BBB* pB = &B;
BBB_X* pBX = &BX;
BBB_Y* pBY = &BY;
//Приводим явно double к int
i = static_cast<int>(d);
//и наоборот
d = static_cast<int>(i);
//указатель на int к char
c = static_cast<char>(pi);//Ошибка!!!
//char к указателю на void
v = static_cast<void*>(c);//Ошибка!!!
//указатель на void к указателю на int
pi = static_cast<int*>(v);
//Снимаем константность const int*
pi = static_cast<int *>(cpi);//Ошибка!!!
//Приводим указатель на объект AAA к указателю на объект BBB
//из разных иерархий
pA = static_cast<AAA*>(pB);//Ошибка!!!
//Приводим указатель на double к double
d = static_cast<double>(pd);//Ошибка!!!
//А если наоборот?
pd = static_cast<double*>(d0);//Ошибка!!!
//Перемещение из одной иерархии наследования в другую
pB = static_cast<BBB*>(pBX);
pBY = static_cast<BBB_Y*>(pB);
return 0;
}
dynamic_cast
Оператор приведения dynamic_cast применяется для полиморфного приведения типов на этапе выполнения программы (класс считается полиморфным, если в нем есть хотя бы одна виртуальная функция). Если указатель, подлежащий приведению, ссылается на объект результирующего класса или объект класса производный от результирующего то приведение считается успешным. То же самое для ссылок. Если приведение невозможно, то на этапе выполнения программы будет возвращен NULL, если приводятся указатели. Если приведение производится над ссылками, то будет сгенерировано исключение std::bad_cast. Несмотря на то, что dynamic_cast предназначен для приведения полиморфных типов по иерархии наследования, он может быть использован и для обычных неполиморфных типов вверх по иерахии. В этом случае ошибка будет получена на этапе компиляции. Оператор приведения dynamic_cast приводить к указателю на void, но не может приводить указатель на void к другому типу. Способность dynamic_cast приводить полиморфные типы обеспечивается системой RTTI (Run-Time Type Identification), которая позволяет идентифицировать тип объекта в процессе выполнения программы. При множественном наследовании dynamic_cast может вернуть указатель не на исходный объект, а на его подобъект.
Общий вид приведения:
dynamic_cast <new_type>(exp)
#include <iostream>
//Пустые классы только
//для теста приведения
struct AAA{
//Сделали полиморфным
virtual void do_some(){};
};
struct BBB{
//Сделали полиморфным
virtual void do_some(){};
};
//Наследники BBB
struct BBB_X:BBB{
};
struct BBB_Y:BBB{
};
int main()
{
//Переменные простых типовы и указатели на переменные простых типов
void* v = NULL;
//Объекты классов
AAA A;
BBB B;
BBB_X BX;
BBB_Y BY;
//Указатели на объекты классов
AAA* pA = &A;
BBB* pB = &B;
BBB_X* pBX = &BX;
BBB_Y* pBY = &BY;
//Приводим указатель на объект AAA к указателю на объект BBB
//из разных иерархий
pA = dynamic_cast<AAA*>(pB);
if (pA == NULL)
{
std::cout<<"FAIL"<<std::endl;//Ошибка на этапе выполнения!!!
}
//Приводим указатель на void к указателю на объект BBB
pB = dynamic_cast<AAA*>(v); //Ошибка на этапе компиляции!!!
//Приводим указатель на BBB к указателю на void
v = dynamic_cast<void*>(pB);
//Перемещение из одной иерархии наследования в другую
pB = dynamic_cast<BBB*>(pBX);
pBY = dynamic_cast<BBB_Y*>(pB);
if (pBY == NULL)
{
std::cout<<"FAIL"<<std::endl;//Ошибка на этапе выполнения!!!
}
system("pause");
return 0;
}
Источники:
Видеолекция Евгения Линского с проекта Лекториум
Блог Алёна С++
Этот пост dreary_eyes
«Полный справочник по C++» Герберт Шилдт
«Дизайн и эволюция языка C++» Бьерн Страуструп
Первоисточник:
Стандарт языка C++ (цена $212)
Бесплатный рабочий проект стандарта языка С++ N3337
Дополнительно:
Изображение взято из поста SOLON7
Комментарии (35)
ilmirus
13.09.2015 04:58+6Указано не все. Раздел 5.4 ISO/IEC 14882 указывает еще и functional notation (5.2.3). То есть, int i = int(a) тоже является кастом.
love_energy
13.09.2015 12:28-3Можно конкретную ссылку? Попытаюсь обработать и добавить, если так.
ilmirus
13.09.2015 16:18Пожалуйста! http://www.open-std.org/JTC1/sc22/WG21/docs/papers/2011/n3242.pdf
Пусть и старая версия, но сойдет.
Eivind
13.09.2015 20:14Является эквивалентом приведения только для простых типов.
Но соглашусь, статья является не полной без описания явных и неявных приведений типов в конструкторах и вызовах функций, из чего и следует данный вариант приведения.love_energy
13.09.2015 20:44Не ставил такой цели при написании поста. Но подумаю о возможном расширении поста в этом ключе.
5nw
13.09.2015 07:30+9Оператор приведения const_cast удаляет квалификаторы const и volatile с исходного типа данных
Не только удаляет, но и может добавлятьlove_energy
13.09.2015 12:26Спасибо. Добавлю. Может еще чего-то не хватает?
5nw
13.09.2015 12:45+1Еще заметил опечатку
dynamic_cast
…
Общий вид приведения:
static_cast<new_type>(exp)
boolivar
13.09.2015 13:15+1Пример бы, да пару слов о назначении, вроде к const из не-const типы приводятся автоматически, это и в примере видно: неконстантные ссылки передаются в функцию требующую константные ссылки.
Про добавление/удаление volatile вообще не слышал.love_energy
13.09.2015 13:22Хорошо. Постараюсь расширить пример в это ключе (добавлениие const, volatile).
5nw
13.09.2015 13:24+1Долго не думал, поэтому пример несколько вырожденный, наверняка можно придумать удачнее
#include <iostream> using namespace std; void f(int *x) { cout << __PRETTY_FUNCTION__ << endl; } void f(const int *x) { cout << __PRETTY_FUNCTION__ << endl; } int main() { int x = 5; int *px = &x; f(px); f(const_cast<const int*>(px)); return 0; }
love_energy
13.09.2015 14:00Спасибо. К моменту как увидел ваш пример, уже добавил свой тоже вырожденный. Как думаете может как-то изменить его?
5nw
13.09.2015 19:56+1Мой пример показывает, для чего может использоваться «добавление» const (для вызова перегруженной функции), а ваш более общий, так что сравнивать наверное не совсем правильно
love_energy
13.09.2015 12:34Вопросы ко всем посмотревшим пост. Может еще что-то в описание добавить? Или в примерах чего-то не хватает?
Nomad1
13.09.2015 12:36+2Не хватает вычитки текста (надо избавиться от «привидения» и «приводить») и ссылок на стандарты языка. Заодно не помешало бы написать, что для dynamic_cast нужны RTTI.
love_energy
13.09.2015 12:49Вы считает, что без «привидения» и «приводить» будет более удобно для прочтения? Не потеряется ли смысл в некоторых местах?
Ссылки на стандарты языка? Но я их не использовал их как источник, как вы видите в списке источников снизу. Такая ссылка будет обманом.
Но может вы дадите мне ссылку где можно скачать и почитать данные стандарты, а лучше те части, где про приведение типов? Я постараюсь их прочитать и скорректировать пост или добавить что-то новое в него.
Про RTTI добавлю.Nomad1
13.09.2015 13:00+2Привидение — это что-то страшное в простыне, или с моторчиком.
приведение dynamic_cast приводить к указателю на void
Особо хорошо было бы, если бы тут тоже было «привидение» :)
А стандарт языка — неоспоримый первоисточник знаний по языку, отсылка на него должна быть в принципе всегда, хотя бы в качестве номера вроде 5.2.7 для dynamic_cast или хоть ссылки в конце статьи.love_energy
13.09.2015 13:14Привидение — это что-то страшное в простыне, или с моторчиком.
Дошло чем вы :) Исправил.
По поводу стандарта согласен. Конечно, это первоисточник. Просто я брал информацию из других источников, которые, я надеюсь, туда заглядывали. Вы правы в том, что корректней было бы написать пост пользуясь только стандартом. Тогда наверно меньше бы было испорченного телефона. Я пока его не читал. Можно ли его скачать бесплатно?Nomad1
13.09.2015 13:26+2Навскидку так нашлось:
www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf
Наверняка есть другие ревизии и более новые ссылки, но базовые вещи не меняются обычно.
Именно зубрить его я не советую, просто поглядывайте туда в случае вопросов или неоднозначностей, в подобных статьях можно вставлять пару цитат из него на английском, тоже полезно будет. Что характерно, компиляторы не всегда на 100% придерживаются стандарта или трактуют по-своему вещи, там не указанные. Например, поведение dynamic_cast с выключенным RTTI разное в MSVC и GCC.
love_energy
13.09.2015 20:50Если у кого-то из вас тех, кто читал пост есть примеры по теме как у пользователя 5nw, которые более полно раскрывают какие-то аспекты, то постите или скидывайте мне в личку и я добавлю их в пост.
potan
14.09.2015 13:45+1При множественном наследовании есть тонкости. static_cast и dynamic_cast могут вернуть указатель не на исходный объект, а на его подобъект.
love_energy
14.09.2015 18:17Вставил ваше дополнение в пост. Это действительно ценное дополнение, если честно я нигде про такое ни читал, а практике ситуация не встречалась. Как вам удалось об это узнать?
potan
14.09.2015 19:19+1Если мне память не изменяет, прочитал в «Дизайн и эволюция языка C++» Страуструппа.
А потом рассуждал, что это значит с точки зрения теории категорий, по этому и запомнил :-).love_energy
15.09.2015 19:30От создателя языка дополнение получается. Эх хорошо бы было, если б он сам заглянул в этот пост и подправил некоторые моменты (-:
0xd34df00d
14.09.2015 21:37Union cast ещё. Хоть там и не все гладко с точки зрения языка, но в реальной жизни встречается.
love_energy
15.09.2015 19:19Вы имеете имеете в виду преобразование типов при помощи union? Почему не все гладко гладко с точки зрения языка? Поясните, пожалуйста.
0xd34df00d
16.09.2015 13:56Да, его.
Если я правильно понимаю стандарт, то писать в одни член union'а, а потом читать из другого — UB. Могу ошибаться, впрочем, по крайней мере, на тему простых случаев для каких-нибудь тривиальных типов. Ссылок на стандарт, к сожалению, сходу не дам — относительно давно этот вопрос изучал.
GamePad64
Это уже было
в Симпсонахна хабре. Да и потом, эта тема разжёвана в любом учебнике по C++.love_energy
Да видел этот пост. Если обратите внимание, то увидите в источниках снизу. И учебник тоже в источниках увидите. Проблема в том, что и в этом посте и в учебнике что-то одно говорится, а что-то упускается. И не получается полной картины.Моя попытка это собрать всю информацию вместе и изложить относительно понятным языком и добавить простые примеры для новичка. Хотя и я, возможно, упустил какие-то детали.Как вы думаете?