Сообщений 9 Оценка 281 [+1/-0] Оценить |
Как подсказывает название, static_cast преобразует выражения одного статического типа в объекты и значения другого статического типа. В соответствии со стандартом C++ (пункт 5.2.9/1-3):
«Результатом выражения static_cast<T>(v) является результат преобразования выражения v к типу T. Если T является ссылкой, то результат lvalue; в противном случае — результат rvalue. В static_cast не должны определяться типы. Оператор static_cast не может снимать константность.
Выражение e может быть явно преобразовано к типу T используя static_cast следующим образом: static_cast<T>(e), если объявление “T t(e);” верно для некоторой воображаемой временной переменной t.
В противном случае static_cast должен выполнить одно из преобразований, указанных ниже. Никаких других преобразований не должно производиться явно при использовании static_cast.»
Это означает, что static_cast допускается, если:
В этом пункте также упоминается, что static_cast «не снимает константность». Стандарт буквально имеет в виду, что оператор static_cast не может убрать спецификатор const. Например, static_cast не может преобразовать char const * в char *, хотя он может преобразовать char * в char const *.
cv-квалификацией принято называть добавление к типу спецификаторов const и/или volatile. Если вы хотите убрать cv-квалификацию, то единственным подходящим оператором приведения типа будет const_cast. Я оставлю подробное обсуждение этого оператора до более основательного рассмотрения правил использования спецификатора const. |
Если даны объявления
struct B { operator int(); }; struct D : B { }; B b; float const f = 0; |
то следующие два преобразования
static_cast<void *>(&b); // эквивалентно '(void *) &b;' static_cast<int>(b); // эквивалентно '(int) b;' and 'int(b);' |
являются допустимыми. Первое преобразование основано на стандартном неявном преобразовании типа B* к типу void*, тогда как второе неявно вызывает b.operator int(). Оба преобразования следуют общему правилу пункта 5.2.9/1 стандарта.
Как уже упоминалось выше, этот пункт определяет ряд исключений из общего правила. Эти исключения допускают преобразования
static_cast<int>(f); // эквивалентно '(int) f;' and 'int(f);' static_cast<D &>(b); // эквивалентно '(D &) b;' |
(Первый из двух примеров здесь не уместен, т.к., согласно стандарту, объект типа int может быть инициализирован значением типа float - прим. RSDN.)
даже если объекты получаемого типа не могут быть инициализированы значениями исходного типа.
В заключение, два выражения
static_cast<int const *>(&f); // OK static_cast<int *>(&f); // ошибка |
(С точки зрения стандарта, эти примеры недопустимы: static_cast не может приводить указатели несвязанных типов, каковыми являются int и float - прим. RSDN.)
Первое из них выполнится успешно, тогда как второе – нет. В обоих выражениях осуществляется попытка преобразовать указатель на переменную типа float в указатель на переменную типа int. Однако второе выражение также убирает спецификатор const, что не допускается стандартом. Второе преобразование можно осуществить более обычным способом
(int *) &f;
|
Пункт 5.2.9/1 допускает преобразование
T t = static_cast<T>(e);
|
если конструкция
T t(e); |
также является допустимой – т.е. если прямая инициализация возможна без приведения типа. Следовательно, объявления
long l = static_cast<long>('x'); |
и
long l('x');
|
являются эквивалентными. Вам может показаться странным, что язык допускает такое избыточное приведение типа, т.к. оно ухудшает код без добавления какой-либо новой возможности.
Я же считаю полезным указывать все преобразования типов явным образом, даже когда это не необходимо. Преобразования типов опасны, вне зависимости от того, допускаются ли они неявно правилами языка, либо явно — операторами преобразования типа и конструкторами. Если вы будете прилежно выделять таким образом все преобразования типов, то сможете легче выявлять потенциальные источники проблем в дальнейшем.
Кто-то предлагал комитету стандартизации C++ добавить пятый оператор приведения типов (implicit_cast) для выделения преобразований, которые язык допускает и без этого. Комитет отклонил это предложение, возможно потому, что шаблон
template<typename FROM, typename TO> inline TO implicit_cast(FROM const &x) { return x; } |
является несложным в написании эквивалентом предложенного оператора. Используя этот шаблон, можно переписать приведенный ранее пример следующим образом:
long l = implicit_cast<long>('x'); |
Если вам не нравится писать static_cast там, где не требуется явного приведения типа, подумайте над добавлением implicit_cast в вашу библиотеку и используйте его там, где возможно.
Если бы оператор static_cast был разрешен только в ситуациях, где достаточно implicit_cast, он потерял бы всякий смысл. Однако, как я показал в предыдущих примерах, static_cast также позволяет производить явные преобразования типа там, где языком не разрешены неявные. Этими допустимыми явными преобразованиями являются:
В соответствии со стандартом, любой тип выражения может быть преобразован к типу void с cv-квалификацией.
Это правило допускает все следующие выражения:
static_cast<void>(5); static_cast<const void>("abc"); static_cast<volatile const void>('x'); |
Вы можете удивиться: зачем вам может понадобиться приводить какой-либо тип к void, особенно учитывая, что вы не можете объявлять объекты типа void. На ум приходят две возможные причины.
Для начала рассмотрим шаблон
template<typename R1, typename R2> R1 adapt(R2 (*f)()) { // ... return static_cast<R1>(f()); } |
adapt принимает в качестве параметра функцию f, которая не имеет параметров и возвращает тип R2. При возврате управления функция adapt вызывает f(), преобразуя возвращаемый f тип R2 к типу, который возвращает сама функция adapt (R1).
Для любознательных: я называю этот шаблон adapt, потому что его паттерн имеет сходство с адаптерами функций-объектов в STL. |
Если вы инстанциируете шаблон функцией
int g(); adapt<void>(g); |
в результате будет создана специализация
void adapt(int (*f)()) { // ... return static_cast<void>(f()); } |
Поскольку static_cast может преобразовать к void, вы можете использовать один шаблон как для типа void, так и для других, отличных от void, типов.
Другой возможной мотивацией для static_cast<void> служит явное игнорирование побочных эффектов выражения. Если вы вызываете функцию
int f();
|
как
f(); |
без использования возвращаемого значения функции, то программисты, сопровождающие код в дальнейшем, будут гадать: собирались ли вы использовать возвращаемое значение, но случайно этого не сделали, или нет. Если же вы явно отбросите возвращаемое значение таким образом
static_cast<void>(f()); |
то другие программисты смогут быть более уверенными в ваших намерениях.
Если даны следующие типы
struct Base { }; struct Derived : Base { }; |
и объекты
Derived derived; Base &base = derived; |
то стандарт позволяет явное преобразование
static_cast<Derived &>(base);
|
Стандарт утверждает, что результат этого приведения — lvalue типа Derived, но я думаю, что результат является – или должен быть – Derived &.
Хотя это приведение является допустимым, оно может привести к неприятностям. Рассмотрим похожий пример
Base base1;
static_cast<Derived &>(base1);
|
Здесь, base1 в действительности является объектом типа Base, а не Derived. Это преобразование «обманывает» компилятор и приводит к неопределенному поведению программы.
Стандарт перечисляет другие ограничения времени компиляции на это преобразование:
Статья 4 стандарта перечисляет множество неявных стандартных преобразований:
Большинство этих преобразований адаптированы из C,и должны быть хорошо вам знакомы. Поскольку преобразования неявные, то они могут осуществляться без какого-либо явного указания о приведении типа. Например
char a[10]; char *p = a; // преобразование массива в указатель char const *s = p; // добавление спецификатора const void *v = p; // преобразование указателей float f = 123; // преобразование между интегральным числом и числом с плавающей точкой unsigned u = 123; // продвижение интегрального типа |
static_cast позволяет вам обратить эти преобразования, заставляя некоторые из них осуществляться противоположным образом. В частности, static_cast может обращать стандартные преобразования интегрального типа и типа с плавающей точкой, указателей и указателей на члены. В примере
char *p; void *v; v = p; // OK, неявное преобразование p = v; // error, нет неявного преобразования p = static_cast<char *>(v); // OK, явное преобразование |
использование static_cast «подавляет» стандартные правила преобразований, вызывая преобразование, иначе невозможное.
static_cast также позволяет проводить обратные преобразования из:
Эти преобразования приводят к неопределенному поведению, если:
Заметьте, что static_cast не обращает все неявные преобразования. В частности, static_cast не может преобразовать из:
Вы можете принудительно выполнить некоторые из этих преобразований с помощью других способов приведения типа. Остальные обращенные преобразования — такие как преобразование указателя обратно в массив — не могут быть выполнены никаким видом приведения типа.
Многие из преобразований, допускаемых оператором static_cast, позволяют преобразовывать базовые классы в производные классы. Подобные преобразования называют нисходящими (downcast), так как они требуют спуска по иерархии наследования. Сравните их с противоположными им восходящими преобразованиями (upcast, от производных к базовым), которые язык позволяет делать неявно.
Нисходящее приведение типа небезопасно, т.к. оно подразумевает, что сущность статического типа, соответствующего базовому классу, в действительности имеет динамический тип производного класса. Если вы осуществляете нисходящее приведение типа с использованием static_cast, компилятор будет считать, что вы правы, даже если это не так. Если же в действительности это не так, результатом будет неопределенное — и подчас весьма неприятное — поведение программы.
Хотя я и не рассматриваю dynamic_cast в этой статье, я хотел бы упомянуть его в качестве альтернативы static_cast для нисходящего приведения типа. Принципиальным преимуществом dynamic_cast является то, что ваша программа проверяет ваше предположение во время исполнения. Если (предположительно) производная сущность в действительности не является объектом производного типа, то dynamic_cast либо вернет NULL (при преобразовании указателей), либо выбросит исключение (при преобразовании ссылок).
Я собираюсь подробно рассмотреть оператор dynamic_cast и ассоциированную с ним идентификацию типов во время выполнения (RTTI — Run-Time Type Identification) в будущих статьях.
В своей следующей статье я подобным образом рассмотрю reinterpret_cast и дам советы, когда использовать его либо static_cast.
Роберт Шмидт (Robert Schmidt) технический автор MSDN. Его другая основная писательская страсть - C/C++ Users Journal, в котором он является выпускающим редактором и ведет свою колонку. Его предыдущие этапы карьеры – радио ди-джей, дрессировщик диких животных, астроном, pool-hall operator, частный сыщик, разносчик газет и преподаватель колледжа.
Сообщений 9 Оценка 281 [+1/-0] Оценить |