Этот пост попытка кратко оформить все, что я читал или слышал из разных источников про операторы приведения типов в языке 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)


  1. GamePad64
    13.09.2015 02:03
    +7

    Это уже было в Симпсонах на хабре. Да и потом, эта тема разжёвана в любом учебнике по C++.


    1. love_energy
      13.09.2015 12:25
      -4

      Да видел этот пост. Если обратите внимание, то увидите в источниках снизу. И учебник тоже в источниках увидите. Проблема в том, что и в этом посте и в учебнике что-то одно говорится, а что-то упускается. И не получается полной картины.Моя попытка это собрать всю информацию вместе и изложить относительно понятным языком и добавить простые примеры для новичка. Хотя и я, возможно, упустил какие-то детали.Как вы думаете?


  1. ilmirus
    13.09.2015 04:58
    +6

    Указано не все. Раздел 5.4 ISO/IEC 14882 указывает еще и functional notation (5.2.3). То есть, int i = int(a) тоже является кастом.


    1. love_energy
      13.09.2015 12:28
      -3

      Можно конкретную ссылку? Попытаюсь обработать и добавить, если так.


      1. ilmirus
        13.09.2015 16:18

        Пожалуйста! http://www.open-std.org/JTC1/sc22/WG21/docs/papers/2011/n3242.pdf
        Пусть и старая версия, но сойдет.


        1. love_energy
          13.09.2015 20:20
          +1

          Спасибо. Изучу.


    1. Eivind
      13.09.2015 20:14

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


      1. love_energy
        13.09.2015 20:44

        Не ставил такой цели при написании поста. Но подумаю о возможном расширении поста в этом ключе.


        1. Eivind
          14.09.2015 00:07

          Посмотрите ещё и cast operator:

          operator T();
          


          1. love_energy
            14.09.2015 18:10

            Обязательно посмотрю.


  1. 5nw
    13.09.2015 07:30
    +9

    Оператор приведения const_cast удаляет квалификаторы const и volatile с исходного типа данных
    Не только удаляет, но и может добавлять


    1. love_energy
      13.09.2015 12:26

      Спасибо. Добавлю. Может еще чего-то не хватает?


      1. 5nw
        13.09.2015 12:45
        +1

        Еще заметил опечатку

        dynamic_cast

        Общий вид приведения:
        static_cast<new_type>(exp)


        1. love_energy
          13.09.2015 12:49

          Исправлю.


    1. boolivar
      13.09.2015 13:15
      +1

      Пример бы, да пару слов о назначении, вроде к const из не-const типы приводятся автоматически, это и в примере видно: неконстантные ссылки передаются в функцию требующую константные ссылки.
      Про добавление/удаление volatile вообще не слышал.


      1. love_energy
        13.09.2015 13:22

        Хорошо. Постараюсь расширить пример в это ключе (добавлениие const, volatile).


      1. 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;
        }
        


        1. love_energy
          13.09.2015 14:00

          Спасибо. К моменту как увидел ваш пример, уже добавил свой тоже вырожденный. Как думаете может как-то изменить его?


          1. 5nw
            13.09.2015 19:56
            +1

            Мой пример показывает, для чего может использоваться «добавление» const (для вызова перегруженной функции), а ваш более общий, так что сравнивать наверное не совсем правильно


            1. love_energy
              13.09.2015 20:34

              Понял вас. Добавил ваш пример в пост. Лишним не будет.


  1. love_energy
    13.09.2015 12:34

    Вопросы ко всем посмотревшим пост. Может еще что-то в описание добавить? Или в примерах чего-то не хватает?


  1. Nomad1
    13.09.2015 12:36
    +2

    Не хватает вычитки текста (надо избавиться от «привидения» и «приводить») и ссылок на стандарты языка. Заодно не помешало бы написать, что для dynamic_cast нужны RTTI.


    1. love_energy
      13.09.2015 12:49

      Вы считает, что без «привидения» и «приводить» будет более удобно для прочтения? Не потеряется ли смысл в некоторых местах?
      Ссылки на стандарты языка? Но я их не использовал их как источник, как вы видите в списке источников снизу. Такая ссылка будет обманом.
      Но может вы дадите мне ссылку где можно скачать и почитать данные стандарты, а лучше те части, где про приведение типов? Я постараюсь их прочитать и скорректировать пост или добавить что-то новое в него.
      Про RTTI добавлю.


      1. Nomad1
        13.09.2015 13:00
        +2

        Привидение — это что-то страшное в простыне, или с моторчиком.

        приведение dynamic_cast приводить к указателю на void


        Особо хорошо было бы, если бы тут тоже было «привидение» :)

        А стандарт языка — неоспоримый первоисточник знаний по языку, отсылка на него должна быть в принципе всегда, хотя бы в качестве номера вроде 5.2.7 для dynamic_cast или хоть ссылки в конце статьи.


        1. love_energy
          13.09.2015 13:14

          Привидение — это что-то страшное в простыне, или с моторчиком.

          Дошло чем вы :) Исправил.
          По поводу стандарта согласен. Конечно, это первоисточник. Просто я брал информацию из других источников, которые, я надеюсь, туда заглядывали. Вы правы в том, что корректней было бы написать пост пользуясь только стандартом. Тогда наверно меньше бы было испорченного телефона. Я пока его не читал. Можно ли его скачать бесплатно?


          1. 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.


            1. love_energy
              13.09.2015 13:57

              Спасибо. Буду изучать.


  1. love_energy
    13.09.2015 20:50

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


  1. potan
    14.09.2015 13:45
    +1

    При множественном наследовании есть тонкости. static_cast и dynamic_cast могут вернуть указатель не на исходный объект, а на его подобъект.


    1. love_energy
      14.09.2015 18:17

      Вставил ваше дополнение в пост. Это действительно ценное дополнение, если честно я нигде про такое ни читал, а практике ситуация не встречалась. Как вам удалось об это узнать?


      1. potan
        14.09.2015 19:19
        +1

        Если мне память не изменяет, прочитал в «Дизайн и эволюция языка C++» Страуструппа.
        А потом рассуждал, что это значит с точки зрения теории категорий, по этому и запомнил :-).


        1. love_energy
          15.09.2015 19:30

          От создателя языка дополнение получается. Эх хорошо бы было, если б он сам заглянул в этот пост и подправил некоторые моменты (-:


  1. 0xd34df00d
    14.09.2015 21:37

    Union cast ещё. Хоть там и не все гладко с точки зрения языка, но в реальной жизни встречается.


    1. love_energy
      15.09.2015 19:19

      Вы имеете имеете в виду преобразование типов при помощи union? Почему не все гладко гладко с точки зрения языка? Поясните, пожалуйста.


      1. 0xd34df00d
        16.09.2015 13:56

        Да, его.

        Если я правильно понимаю стандарт, то писать в одни член union'а, а потом читать из другого — UB. Могу ошибаться, впрочем, по крайней мере, на тему простых случаев для каких-нибудь тривиальных типов. Ссылок на стандарт, к сожалению, сходу не дам — относительно давно этот вопрос изучал.