Еще раз про приведение типов в языке С++ или расстановка всех точек над cast

  • Tutorial


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

Поддержать автора
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 36

    +7
    Это уже было в Симпсонах на хабре. Да и потом, эта тема разжёвана в любом учебнике по C++.
      –4
      Да видел этот пост. Если обратите внимание, то увидите в источниках снизу. И учебник тоже в источниках увидите. Проблема в том, что и в этом посте и в учебнике что-то одно говорится, а что-то упускается. И не получается полной картины.Моя попытка это собрать всю информацию вместе и изложить относительно понятным языком и добавить простые примеры для новичка. Хотя и я, возможно, упустил какие-то детали.Как вы думаете?
      +6
      Указано не все. Раздел 5.4 ISO/IEC 14882 указывает еще и functional notation (5.2.3). То есть, int i = int(a) тоже является кастом.
        –3
        Можно конкретную ссылку? Попытаюсь обработать и добавить, если так.
          0
          Пожалуйста! http://www.open-std.org/JTC1/sc22/WG21/docs/papers/2011/n3242.pdf
          Пусть и старая версия, но сойдет.
            +1
            Спасибо. Изучу.
          0
          Является эквивалентом приведения только для простых типов.
          Но соглашусь, статья является не полной без описания явных и неявных приведений типов в конструкторах и вызовах функций, из чего и следует данный вариант приведения.
            0
            Не ставил такой цели при написании поста. Но подумаю о возможном расширении поста в этом ключе.
              0
              Посмотрите ещё и cast operator:
              operator T();
              
                0
                Обязательно посмотрю.
          +9
          Оператор приведения const_cast удаляет квалификаторы const и volatile с исходного типа данных
          Не только удаляет, но и может добавлять
            0
            Спасибо. Добавлю. Может еще чего-то не хватает?
              +1
              Еще заметил опечатку
              dynamic_cast

              Общий вид приведения:
              static_cast<new_type>(exp)
                0
                Исправлю.
              +1
              Пример бы, да пару слов о назначении, вроде к const из не-const типы приводятся автоматически, это и в примере видно: неконстантные ссылки передаются в функцию требующую константные ссылки.
              Про добавление/удаление volatile вообще не слышал.
                0
                Хорошо. Постараюсь расширить пример в это ключе (добавлениие const, volatile).
                  +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;
                  }
                  
                    0
                    Спасибо. К моменту как увидел ваш пример, уже добавил свой тоже вырожденный. Как думаете может как-то изменить его?
                      +1
                      Мой пример показывает, для чего может использоваться «добавление» const (для вызова перегруженной функции), а ваш более общий, так что сравнивать наверное не совсем правильно
                        0
                        Понял вас. Добавил ваш пример в пост. Лишним не будет.
                0
                Вопросы ко всем посмотревшим пост. Может еще что-то в описание добавить? Или в примерах чего-то не хватает?
                  +2
                  Не хватает вычитки текста (надо избавиться от «привидения» и «приводить») и ссылок на стандарты языка. Заодно не помешало бы написать, что для dynamic_cast нужны RTTI.
                    0
                    Вы считает, что без «привидения» и «приводить» будет более удобно для прочтения? Не потеряется ли смысл в некоторых местах?
                    Ссылки на стандарты языка? Но я их не использовал их как источник, как вы видите в списке источников снизу. Такая ссылка будет обманом.
                    Но может вы дадите мне ссылку где можно скачать и почитать данные стандарты, а лучше те части, где про приведение типов? Я постараюсь их прочитать и скорректировать пост или добавить что-то новое в него.
                    Про RTTI добавлю.
                      +2
                      Привидение — это что-то страшное в простыне, или с моторчиком.
                      приведение dynamic_cast приводить к указателю на void


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

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

                        Дошло чем вы :) Исправил.
                        По поводу стандарта согласен. Конечно, это первоисточник. Просто я брал информацию из других источников, которые, я надеюсь, туда заглядывали. Вы правы в том, что корректней было бы написать пост пользуясь только стандартом. Тогда наверно меньше бы было испорченного телефона. Я пока его не читал. Можно ли его скачать бесплатно?
                          +2
                          Навскидку так нашлось:
                          www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3337.pdf

                          Наверняка есть другие ревизии и более новые ссылки, но базовые вещи не меняются обычно.
                          Именно зубрить его я не советую, просто поглядывайте туда в случае вопросов или неоднозначностей, в подобных статьях можно вставлять пару цитат из него на английском, тоже полезно будет. Что характерно, компиляторы не всегда на 100% придерживаются стандарта или трактуют по-своему вещи, там не указанные. Например, поведение dynamic_cast с выключенным RTTI разное в MSVC и GCC.
                            0
                            Спасибо. Буду изучать.
                    0
                    Если у кого-то из вас тех, кто читал пост есть примеры по теме как у пользователя 5nw, которые более полно раскрывают какие-то аспекты, то постите или скидывайте мне в личку и я добавлю их в пост.
                      +1
                      При множественном наследовании есть тонкости. static_cast и dynamic_cast могут вернуть указатель не на исходный объект, а на его подобъект.
                        0
                        Вставил ваше дополнение в пост. Это действительно ценное дополнение, если честно я нигде про такое ни читал, а практике ситуация не встречалась. Как вам удалось об это узнать?
                          +1
                          Если мне память не изменяет, прочитал в «Дизайн и эволюция языка C++» Страуструппа.
                          А потом рассуждал, что это значит с точки зрения теории категорий, по этому и запомнил :-).
                            0
                            От создателя языка дополнение получается. Эх хорошо бы было, если б он сам заглянул в этот пост и подправил некоторые моменты (-:
                        0
                        Union cast ещё. Хоть там и не все гладко с точки зрения языка, но в реальной жизни встречается.
                          0
                          Вы имеете имеете в виду преобразование типов при помощи union? Почему не все гладко гладко с точки зрения языка? Поясните, пожалуйста.
                            0
                            Да, его.

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

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

                          Самое читаемое