Что такое static cast
Приведение типов
Будучи на конференции Qt Developer Days 2010 я узнал, что одним из самых популярных вопросов на собеседовании в разные зарубежные компании, работающие с Qt библиотекой, является вопрос о различиях в способах приведения типов в C++. Поэтому здесь я рассмотрю основные различия между static_cast, dynamic_cast, const_cast, reinterpret_cast, C-style cast, qobject_cast и qvariant_cast
static_cast преобразует выражения одного статического типа в объекты и значения другого статического типа. Поддерживается преобразование численных типов, указателей и ссылок по иерархии наследования как вверх, так и вниз. Проверка производится на уровне компиляции, так что в случае ошибки сообщение будет получено в момент сборки приложения или библиотеки.
Используется для динамического приведения типов во время выполнения. В случае неправильного приведения типов для ссылок вызывается исключительная ситуация std::bad_cast, а для указателей будет возвращен 0. Использует систему RTTI (Runtime Type Information). Безопасное приведение типов по иерархии наследования, в том числе для виртуального наследования.
Пожалуй самое простое приведение типов. Снимает cv qualifiers — const и volatile, то есть константность и отказ от оптимизации компилятором переменной. Это преобразование проверяется на уровне компиляции и в случае ошибки приведения типов будет выдано сообщение.
Приведение типов без проверки. reinterpret_cast — непосредственное указание компилятору. Применяется только в случае полной уверенности программиста в собственных действиях. Не снимает константность и volatile. применяется для приведения указателя к указателю, указателя к целому и наоборот.
Си-шный метод приведения типов. Пожалуй самый нежелательный способ приведения типов. Страуструп пишет:
«Например, что это значит выражение — x = (T)y;. Мы не знаем. Это зависит от типа T, типов x и y. T может быть названием типа, typedef или может быть параметр template-а. Может быть, х и у являются скалярными переменными и Т представляет собой значение преобразования. Может быть, х объекта класса, производного от класса Y и Т — нисходящее преобразование. По этой причине программист может не знать, что он делает на самом деле.»
Вторая причина нежелательного использования приведения типов в C-style — трудоемкость процесса поиска мест приведения типов.
Приводит объект QObject* к типу TYPE если объект типа объекта TYPE или тип наследует от TYPE иначе возвращает 0. qobject_cast от 0 также дает 0. Необходимое условие. Класс должен наследовать от QObject и содержать в себе макрос Q_OBJECT. Функция ведет себя аналогично стандартному dynamic_cast, но при этом не использует RTTI. Вот как описана данная функция в Qt 4.7.0:
Итак, что тут происходит:
Во-первых если не определены QT_NO_MEMBER_TEMPLATES (определяется только в том случае, если используется версия Microsoft Visual Studio ниже 2002) и QT_NO_QOBJECT_CHECK (определяется в случае использования версии Microsoft Visual Studio ниже 2003), то происходит проверка наличия макроса Q_OBJECT в объявлении класса. И после этого выполняется непосредственно само преобразование — сначала получаем статический объект класса QMetaObject, который называется staticMetaObject, у которого вызывается метод cast, который возвращает const_cast переданного ему объекта, попутно проверяя наследуется ли данный объект от QObject. Далее полученному объекту делается static_cast и возвращается результат.
Приводит объект класса QVariant к нужному классу. Функция аналогична функции qVariantValue.
Рассмотрим, что происходит внутри:
В первой секции кода производится получение идентификатора класса через метасистему Qt. В том случае если класс не зарегистрирован через Q_DECLARE_METATYPE, компиляция кода с приведением к этому типу выдаст ошибку. Далее, если тип объекта, полученный от метасистемы совпадает с типом в значении QVariant, производится reinterpret_cast содержимого объекта, если идентификатор класса не является встроенным типом и его id не совпадает с заложенным в значении QVariant, то возвращается TYPE(). Для случаев, когда мы приводим к встроенному типу, вызывается функция qvariant_cast_helper, которая вызывает в свою очередь функцию convert, адрес которой хранится в структуре Handler. В ней уже осуществляется приведение способом подходящим для типа TYPE. Если конвертация не удалась возвращается объект TYPE()
UPD: Спасибо BaJlepa:
1. const_cast также умеет добавлять cv-квалификаторы
2. для преобразования указателей лучше использовать двойной static_cast через void* вместо reinterpret_cast, потому как такое преобразование позволяет быть уверенным в том, что только pointer-ы участвуют в приведении
Преобразования типов и безопасность типов
В этом документе описаны распространенные проблемы преобразования типов и описывается, как избежать их использования в коде C++.
Когда компилятор обнаруживает ненадежное преобразование, он выдает ошибку или предупреждение. Произошла ошибка при остановке компиляции. Предупреждение позволяет продолжить компиляцию, но указывает на возможную ошибку в коде. Однако даже если программа компилируется без предупреждений, она по-прежнему может содержать код, который вызывает неявные преобразования типов, приводящие к неправильным результатам. Ошибки типов также могут вводиться явными преобразованиями или приведениями в коде.
Неявные преобразования типов
Если выражение содержит операнды различных встроенных типов и явные приведения отсутствуют, компилятор использует встроенные стандартные преобразования для преобразования одного из операндов, чтобы типы совпадали. Компилятор пытается выполнить преобразования в четко определенной последовательности, пока она не завершится успешно. Если выбранное преобразование является повышением, компилятор не выдает предупреждение. Если преобразование является узким, компилятор выдает предупреждение о возможной утрате данных. Происходит ли фактическая потери данных, зависит от фактических значений, но рекомендуется считать это предупреждение как ошибку. Если включен определяемый пользователем тип, компилятор пытается использовать преобразования, указанные в определении класса. Если не удается найти допустимое преобразование, компилятор выдает ошибку и не компилирует программу. Дополнительные сведения о правилах, регулирующих стандартные преобразования, см. в разделе стандартные преобразования. Дополнительные сведения о пользовательских преобразованиях см. в разделе пользовательские преобразования (C++/CLI).
Расширяющие преобразования (продвижение)
В расширяющем преобразовании значение меньшей переменной присваивается более крупной переменной без потери данных. Поскольку расширяющие преобразования всегда являются надежными, компилятор выполняет их автоматически и не выдает предупреждения. Следующие преобразования являются расширяющими преобразованиями.
Сужающие преобразования (приведение)
Компилятор выполняет сужающие преобразования неявным образом, но предупреждает о возможной потере данных. Выведите эти предупреждения очень серьезно. Если вы уверены, что не произойдет потери данных, так как значения в переменной большего размера всегда помещаются в меньшую переменную, добавьте явное приведение, чтобы компилятор больше не выдавал предупреждение. Если вы не уверены, что преобразование является надежным, добавьте в код какую-либо проверку среды выполнения для обработки возможной потери данных, чтобы она не вызывала неправильные результаты.
Преобразование из типа с плавающей запятой в целочисленный тип является узким преобразованием, так как дробная часть значения с плавающей запятой отбрасывается и теряется.
В следующем примере кода показаны некоторые неявные сужающие преобразования и предупреждения, которые возникают компилятором.
Преобразования со знаком — без знака
Компилятор не предупреждает о неявных преобразованиях между целыми типами со знаком и без знака. Поэтому рекомендуется полностью избегать беззнаковых преобразований. Если вы не можете избежать их, добавьте проверку среды выполнения, чтобы определить, является ли преобразуемое значение большим или равным нулю и меньше или равно максимальному значению типа со знаком. Значения в этом диапазоне будут передаваться из входных файлов в неподписанный или из неподписанных в подписывание без переинтерпретации.
Преобразования указателей
Явные преобразования (приведения)
С помощью операции приведения можно указать компилятору преобразовать значение одного типа в другой тип. В некоторых случаях компилятор вызовет ошибку, если эти два типа полностью не связаны, но в других случаях не вызывает ошибку, даже если операция не является строго типизированной. Используйте приведение с осторожностью, так как любое преобразование из одного типа в другой является потенциальным источником ошибок программы. Однако иногда требуется выполнить приведения, а не все приведения являются опасными. Одно эффективное использование приведения заключается в том, что в коде выполняется понижающие преобразования и известно, что преобразование не приводит к созданию неверных результатов в программе. Фактически, это говорит компилятору о том, что вы делаете, а также о том, что вы выполняете предупреждения. Другой способ заключается в приведении из класса указателя на класс, производный от указатель на базовый. Другой способ — приведение к переменной постоянной, чтобы передать ее в функцию, для которой требуется аргумент, не являющийся константой. Большинство этих операций приведения к некоторым рискам требует определенного риска.
В программировании в стиле C для всех типов приведений используется один и тот же оператор приведения в стиле C.
Этот оператор приведения не используется так часто, как другие, и не гарантирует перенос в другие компиляторы.
Дополнительные сведения см. в разделе оператор.
Приведение типов. Наглядное отличие static_cast от dynamic_cast
Доброго времени суток. Очень много статей в интернете о разнице операторов приведения типов, но понимания в данной теме они мне не особо то и не добавили. Пришлось разбираться самому. Хочу поделиться с вами моим опытом на довольно наглядном примере.
Статья рассчитана на тех, кто хочет осознать приведение типов в С++.
Итак, пусть у нас есть такая иерархия наследования:
На картинке изображена иерархия наследования и расположение членов-данных наследников в памяти
Небольшое отступление: почему так важно преобразование типов? Говоря по рабоче-крестьянски, при присваивании объекту типа X объект типа Y, мы должны определить, какое значение будет иметь после присваивания объект типа X.
Начнем с использования static_cast:
Почему таков эффект при выводе значений указателей (значение указателя это адрес, по которому лежит переменная)? Дело в том, что static_cast производит сдвиг указателя.
Рассмотрим на примере:
1. Происходит преобразование типа из C* в D*. Результатом этого есть указатель типа D* (назовем его tempD), который указывает (внимание!) на ту часть в объекте класса C, которая унаследована от класса D. Значение самого pC не меняется!
2. Теперь присваиваем указателю pD значение указателя tempD (всё хорошо, типы одинаковы)
Разумный вопрос: а зачем собственно нужно сдвигать указатель? Говоря по простому, указатель класса D* руководствуется определением класса D. Если бы не произошло смещения, то меняя значения переменных через указатель D, мы бы меняли переменные объекта класса С, которые не относятся к переменным, унаследованным от класса D (если бы указатель pD имел то же значение, что и pC, то при обращении pD->f в действительности мы бы работали с переменной
а).
Промежуточный итог: static_cast при работе с иерархией классов определяет значения указателей так, чтобы обращение к переменным класса через указатель было корректным.
Поговорим о недостатках static_cast. Вернемся к той же иерархии наследования.
Рассмотрим такой код:
Почему pC->f имеет значение отличное от 0? Рассмотрим код по строчкам:
Теперь если мы хотим сделать запись в переменную g через указатель pB (ведь pB полностью уверен что указывает на объект типа B), мы на самом деле запишем данные в переменную f, унаследованную от класса D. Причем указатель pD будет интерпретировать информацию, записанную в переменную f, как float, что мы и видим при выводе через cout.
Как решить такую проблему?
Для этого следует использовать dynamic_cast, который проверяет не только валидность иерархии классов, но и тот факт, что указатель действительно указывает на объект того типа, к которому мы хотим привести.
Для того, чтобы такая проверка была возможна, следует добавить к классам виртуальность (dynamic_cast использует таблицы виртуальных функций, чтобы делать проверку).
Демонстрация решения проблемы, при той же иерархии классов:
Предлагаю запустить код и убедиться, что операция
не получится (потому что pA указывает на объект типа С, что и проверил dynamic_cast и вынес свой вердикт).
Ссылок никаких не привожу, источник — личный опыт.
Еще раз про приведение типов в языке С++ или расстановка всех точек над cast
Этот пост попытка кратко оформить все, что я читал или слышал из разных источников про операторы приведения типов в языке C++. Информация ориентирована в основном на тех, кто изучает C++ относительно недолго и, как мне кажется, должна помочь понять cпецифику применения данных операторов. Старожилы и гуру С++ возможно помогут дополнить или скорректировать описанную мной картину. Всех интересующихся приглашаю под кат.
Приведение типов в стиле языка C (C-style cast)
Приведение типов в стиле языка C может привести выражение любого типа к любому другому типу данных (исключение это приведение пользовательских типов по значению, если не определены правила их приведения, а также приведение вещественного типа к указателю или наоборот). К примеру, unsigned int может быть преобразован к указателю на double. Данный метод приведения типов может быть использован в языке C++. Однако, метод приведения типов в стиле языка C не делает проверки типов на совместимость, как это могут сделать static_cast и dynamic_cast на этапе компиляции и на этапе выполнения соответственно. При этом все, что умеют const_cast и reinterpret_cast данный метод приведения типов делать может.
Общий вид приведения:
, где new_type – новый тип, к которому приводим, а exp – выражение, которое приводится к новому типу.
Т.к. данный оператор не имеет зарезервированного ключевого слова (например, static_cast) найти все места приведения типов в тексте программы будет не очень удобно, если это потребуется.
const_cast
Оператор приведения const_cast удаляет или добавляет квалификаторы const и volatile с исходного типа данных (простые типы, пользовательские типы, указатели, ссылки). Например, был const int, а после преобразования стал int или наоборот. Квалификаторы const и volatile называют cv-квалификаторы (cv-qualifiers). Данные квалификаторы указываются перед именами типов. Как ни трудно догадаться квалификатор const задает константность, т.е. защищает переменную от изменения. Квалификатор volatile говорит о том, что значение переменной может меняться без явного выполнения присваивания. Это обеспечивает защиту от оптимизации компилятором операций с данной переменной.
Общий вид приведения:
Дополнительный пример от пользователя 5nw
Квалификаторы 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 нельзя.
Общий вид приведения:
static_cast
Оператор приведения static_cast применяется для неполиморфного приведения типов на этапе компиляции программы. Отличие static_cast от приведения типов в стиле языка C состоит в том, что данный оператор приведения может отслеживать недопустимые преобразования, такие как приведение указателя к значению или наоборот (unsigned int к указателю на double не приведет), а также приведение указателей и ссылок разных типов считается корректным только, если это приведение вверх или вниз по одной иерархии наследования классов, либо это указатель на void. В случае фиксации отклонения от данных ограничений будет выдана ошибка при компиляции программы. При множественном наследовании static_cast может вернуть указатель не на исходный объект, а на его подобъект.
Общий вид приведения:
dynamic_cast
Оператор приведения dynamic_cast применяется для полиморфного приведения типов на этапе выполнения программы (класс считается полиморфным, если в нем есть хотя бы одна виртуальная функция). Если указатель, подлежащий приведению, ссылается на объект результирующего класса или объект класса производный от результирующего то приведение считается успешным. То же самое для ссылок. Если приведение невозможно, то на этапе выполнения программы будет возвращен NULL, если приводятся указатели. Если приведение производится над ссылками, то будет сгенерировано исключение std::bad_cast. Несмотря на то, что dynamic_cast предназначен для приведения полиморфных типов по иерархии наследования, он может быть использован и для обычных неполиморфных типов вверх по иерархии. В этом случае ошибка будет получена на этапе компиляции. Оператор приведения dynamic_cast может приводить указатель на полиморфный тип к указателю на void, но не может приводить указатель на void к другому типу. Способность dynamic_cast приводить полиморфные типы обеспечивается системой RTTI (Run-Time Type Identification), которая позволяет идентифицировать тип объекта в процессе выполнения программы. При множественном наследовании dynamic_cast может вернуть указатель не на исходный объект, а на его подобъект.
Оператор static_cast
Преобразует выражение в тип типа-ID, основанный только на типах, имеющихся в выражении.
Синтаксис
Remarks
В стандартном языке C++, проверка типа во время выполнения не выполняется, что обеспечивает безопасность преобразования. В C ++/CX выполняются проверки во время компиляции и во время выполнения. Дополнительные сведения см. в разделе Приведение.
static_cast Оператор можно использовать для таких операций, как преобразование указателя на базовый класс в указатель на производный класс. Такие преобразования не всегда являются безопасными.
dynamic_cast Операторы и static_cast перемещают указатель на всю иерархию классов. Однако static_cast полагается исключительно на информацию, предоставленную в инструкции CAST, и поэтому может быть ненадежной. Пример:
Если pb указывает на объект типа, B а не на полный D класс, то dynamic_cast будет достаточно, чтобы вернуть ноль. Однако static_cast полагается на утверждение программиста, которое pb указывает на объект типа D и просто возвращает указатель на этот предполагаемый D объект.
Следовательно, static_cast может выполнить обратное преобразование неявных преобразований, в этом случае результаты будут неопределенными. Программисту остается убедиться, что результаты static_cast преобразования являются надежными.
static_cast Оператор также можно использовать для выполнения любого неявного преобразования, включая стандартные преобразования и пользовательские преобразования. Пример:
static_cast Оператор может явно преобразовать целочисленное значение в тип перечисления. Если значение типа целого не оказывается в диапазоне значений перечисления, получаемое значение перечисления не определено.
static_cast Оператор преобразует нулевое значение указателя в значение указателя null целевого типа.
C++/CLI: Из-за опасности возникновения непроверенных приведений на вершине повторного обнаружения сборщика мусора использование класса должно быть только в критическом для производительности коде, только если вы уверены, что он будет работать правильно. Если необходимо использовать static_cast в режиме выпуска, замените его static_cast в отладочных сборках, чтобы убедиться в успешном выполнении.