10 мая 2012

#7 Операторы приведения в стиле C++

... или «жертвуем красотой и скоростью во имя стабильности»


Кристофера Робина пришлось убрать, поскольку операторов
приведения всего 4. Почему мальчика? Его было проще всего.
Интересно, но в 2020 животные уже человека бы отправили
в плавание, ибо уже не свиной грипп актуален, а корона-вирус
Cat* pCat = (Cat*)pMammal;
Этот способ приведения в стиле C ужасен. Так говорят все признанные специалисты и опытные программисты. Он не осуществляет никаких проверок и малозаметный, в коде его тяжело быстро идентифицировать, потому сложно обнаружить ошибку им генерируемую.

Кстати, в C++ можно использовать старый тип приведения с помощью другой формы записи:
typedef Cat* PCat;
Cat* pCat = PCat(pMammal);
Если мы используем преобразование в старом стиле, то компилятор вообще никогда не будет находить ошибки.

Новые же операторы работают медленнее (во многих случаях), плюс имеют настолько топорный вид в коде, что не заметить его невозможно. При чтении и поддержке кода — очевидный плюс. Они отвечают только за своё преобразование, и потому их приходится комбинировать (включать друг в друга), если мы хотим добиться сложного преобразования. Итак, операторы следующие:
const_cast<>
reinterpret_cast<>
static_cast<>
dynamic_cast<>

О их особенностях читайте в продолжении.
...


—  const_cast добавляет или удаляет квалификаторы const и volatile из полученного выражения. Он безопаснее, чем приведение в старом стиле, потому что отвечает только за указанные квалификаторы. Если преобразовать не получится, то мы узнаем об этом во время компиляции.


—  static_cast служит для приведения «вниз» по иерархии наследования (от базового к производному) по указателю или ссылке.
Mammal* pm = new Cat;

// Нормально — нисходящее приведение.
Cat* pc = static_cast<Cat*>(pm); 
На этапе компиляции проверяется, действительно ли приводимый объект состоит в одной иерархии с объектом, который получим в результате приведения. Если нет — получим ошибку компиляции. Во время работы программы, если объект действительно является тем, что мы хотим получить, то преобразование пройдёт успешно и работа с указателем или ссылкой будут безопасны. Однако, если объект будет другим (тоже наследником этого класса), то программа вылетит с ошибкой во время выполнения. Т.е. данный оператор работает статически, во время компиляции, а не во время выполнения (откуда и получил название). Он не возвращает nullptr, если преобразование указателей не прошло успешно. В случае со ссылками сразу же имеем неопределенное поведение.


—  dynamic_cast так же как и static_cast используется для нисходящего приведения, однако может применяться только к полиморфным классам (т.е. тип приводимого выражения должен быть указателем или ссылкой на класс, в котором есть как минимум одна виртуальная функция). Правильность выполнения проверяется во время работы программы (выполняется динамически). Если не удалось преобразовать указатели, то оператор вернёт nullptr. Если же не удалось преобразовать ссылки, то генерируется исключение std::bad_cast. Этот оператор предполагает существенные издержки времени. dynamic_cast успешно выполняется, если объект фактически является тем, во что мы хотим преобразовать. Пример:

Имеем такую иерархию и следующий код:
Mammal* pm = new IrishSetter;

// Выполнится успешно. pm действительно указывает на собаку. 
Dog* pd = dynamic_cast<Dog*>(pm);

// Выполниться успешно. pm действительно указывает на ирландского сеттера. 
IrishSetter* pis = dynamic_cast<IrishSetter*>(pm);

// Указатель ph будет содержать nullptr. Немного не то млекопитающее! 
Human* ph = dynamic_cast<Human*>(pm);

// Удивительно, но и это сработает правильно!
// Указатель pt не будет нулевым.
// Несмотря на то, что мы имеем всего лишь указатель на
// млекопитающее, полиморфный класс IrishSetter так же
// является хвостатым (Tailed) благодаря тому, что наследуется
// от Dog, который в свою очередь наследуется и от Mammal и
// от Tailed.
Tailed* pt = dynamic_cast<Tailed*>(pm);
Ещё следует отметить, что приведение в стиле C умеет проводить все виды конвертаций, кроме конвертаций, которые выполняет dynamic_cast.


—  reinterpret_cast работает с битами, используется для приведения из одного типа к другому, казалось бы, никак не связанному с первым. Обычно, для сохранения адреса указателей в целочисленных переменных. Поведение программы при выполнении этого преобразования может быть неопределённым. При работе с иерархиями (при нисходящем приведении) этот оператор (в отличие от static_cast и dynamic_cast, которые работают правильно) считает, что указатель на базовый класс — это указатель на производный класс и никак его не преобразует, что является ошибкой.