Сообщений 9    Оценка 281 [+1/-0]         Оценить  
Система Orphus

Deep C++. Operation: static_cast

Автор: Robert Schmidt
Microsoft Corporation

Перевод: Igor Sukharev
Источник: MSDN
Опубликовано: 30.09.2002
Исправлено: 13.03.2005
Версия текста: 1.0
Обзор
Примеры
Неявное приведение типа
Явное приведение типа
К типу void
Базовый тип к ссылке производного типа
Инверсия стандартных преобразований
Альтернатива нисходящему приведению типа
Далее

Обзор

Как подсказывает название, static_cast преобразует выражения одного статического типа в объекты и значения другого статического типа. В соответствии со стандартом C++ (пункт 5.2.9/1-3):

Это означает, что 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

В соответствии со стандартом, любой тип выражения может быть преобразован к типу 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]         Оценить