C ++, будучи строго типизированным языком, строг со своими типами. И всегда бывают случаи, когда нужно преобразовать один тип в другой, что называется приведением. Иногда приведение выполняется неявно. Например -
int a = 10; long b = a;
Это называется неявным преобразованием. Более конкретно, приведенный выше пример представляет собой стандартное преобразование, которое происходит автоматически между основными типами (int
в short
, int
в float
, int
в bool
и т. Д.) И несколькими указателями.
Также могут быть неявные преобразования между классами с преобразованиями конструкторов или операторов. Например -
struct A {};
struct B {
B (A a) {}
};
A a;
B b = a;
Здесь произошло неявное преобразование из A
в B
, потому что B
имеет конструктор, который принимает объект A
в качестве параметра.
Но в некоторых случаях неявное преобразование не работает. Случаи включают случаи, когда мы хотим интерпретировать объект одного типа как другой. В зависимости от вариантов использования C ++ предлагает несколько видов оружия -
static_cast
dynamic_cast
const_cast
reinterpret_cast
- Приведение стиля C и приведение стиля функции
Мы рассмотрим их один за другим и объясним каждый.
static_cast
static_cast
можно использовать для преобразования между указателями на связанные классы (вверх или вниз по иерархии наследования). Он также может выполнять неявные преобразования.
Рассмотрим этот пример -
class Mammal{}; class Human: public Mammal{}; Human *h = new Human; // Pointer to object of derived type Mammal *m = static_cast<Mammal *>(h); // cast it to pointer to base type. static_cast here is unnecessary Mammal *m2 = static_cast<Human *>(m); // cast back to pointer to derived type
Когда мы создаем иерархию, static_cast
не нужен. Каждый Human
- это Mammal
. Таким образом, Human *
можно неявно преобразовать в Mammal *
. static_cast фактически выполнит это неявное приведение, если вы все равно его используете.
Но каждый Mammal
не может быть Human
. Поэтому неявное преобразование из Mammal *
в Human *
не допускается. Вот тут-то и пригодится static_cast
. Если мы опустим статическое приведение, мы получим ошибку в строке
invalid conversion from ‘Mammal*’ to ‘Human*’
Но, используя static_cast
, мы все равно можем выполнить преобразование. static_cast
не выполняет никаких проверок. Поэтому обязанность программистов - убедиться, что преобразование было правильным. Недопустимое преобразование может не завершиться ошибкой, но может вызвать проблемы позже, когда указатель указывает на неполный тип и разыменовывается. Рассмотрим этот пример -
class Mammal {}; class Human: public Mammal { public: virtual void scream() { // Note the virtual std::cout << "MOM" << std::endl; } }; Mammal *m = new Mammal; // Mammal that is not a human! Human *h = static_cast<Human *>(m); // OK so far h -> scream(); // Mayhem!!!
И это не так!
Поэтому не используйте static_cast
для понижения иерархии, если не уверены, что преобразование допустимо.
static_cast
не позволит вам конвертировать между двумя несвязанными классами -
class A {}; class B {}; A *a = new A; B *b = static_cast<A *>(a);
Выдает ошибку -
cannot convert 'A*' to 'B*' in initialization
dynamic_cast
dynamic_cast
относится к static_cast
в том смысле, что помогает преобразовать нас через наследование, но он более мощный, чем static_cast
, но имеет накладные расходы. Он выполняет проверку, чтобы предотвратить описанный выше случай.
Рассмотрим этот пример -
class Mammal{}; class Human: public Mammal{}; Human *h = new Human; // Pointer to object of derived type Mammal *m = dynamic_cast<Mammal *>(h); // ok Human *h1 = dynamic_cast<Human *>(m) // Error
Второе преобразование приведет к ошибке компиляции, поскольку преобразование базового в производное недопустимо с dynamic_cast
, если базовый класс не является полиморфным (имеет хотя бы одну virtual
функцию, объявленную или посредством наследования).
Итак, давайте сделаем базу полиморфной и посмотрим, что получится -
class Mammal { public: virtual void scream() {} }; class Human: public Mammal { public: void scream() override { std::cout << "MOM" << std::endl; } }; Human *h = new Human; Mammal *m = dynamic_cast<Mammal *>(h); Human *h1 = dynamic_cast<Human *>(m); h1 -> scream();
Это работает, как и следовало ожидать.
Что произойдет, если мы попытаемся преобразовать Mammal *
в Human *
, где Mammal
на самом деле не Human
? (который разбил static_cast
один) -
Mammal *m = new Mammal; Human *h = dynamic_cast<Human *>(m); if(h == nullptr) std::cout << "Oops! Cast failed!" << std::endl; else h -> scream();
Это дает результат -
Oops! Cast failed
Итак, как видите, dynamic_cast
выполняет проверку. Он возвращает nullptr
, если вы пытаетесь преобразовать указатели, или выдает std::bad_cast
, если вы пытаетесь преобразовать ссылки.
Но помните, что эта проверка происходит во время выполнения, а не во время компиляции. Он требует информации о типе времени выполнения (RTTI) для отслеживания динамических типов и, следовательно, имеет небольшие накладные расходы.
Давайте теперь посмотрим, что произойдет, когда мы попытаемся преобразовать два несвязанных класса -
class A{ public: void f() {} }; class B{}; A *a = new A; B *b = dynamic_cast<A *>(a);
Это не приводит к ошибке компиляции (в отличие от static_cast
, потому что проверка выполняется во время выполнения (в этот момент b
будет nullptr
).
Наконец, еще одна вещь, которую может сделать dynamic_cast
, - это "боковой заброс". Чтобы понять это, рассмотрим эту классическую иерархию «ужасных бриллиантов» -
struct A { virtual void f() = 0; }; struct L: A { virtual void f() override { std::cout << "Left" << std::endl; } }; struct R: A { virtual void f() override { std::cout << "Right" << std::endl; } }; struct D: L, R {};
Здесь A
- базовый класс. L
и R
наследуются от A
, а D
наследуются от L
и R
.
Под побочным преобразованием мы подразумеваем, что мы должны иметь возможность преобразовывать объект типа L
в тип R
, и он должен вести себя точно так же, как тип R
(и наоборот). Конечно, это возможно только в том случае, если базовый объект действительно имеет тип D
. static_cast
, однако, здесь нам не помогут. Рассмотрим этот код -
D *d = new D; // most derived type L *l = d; // Left type R *r = d; // Right type A *bl = l; // Base class through left A *br = r; // Base class through right br -> f(); // prints "Right" static_cast<L *>(br) -> f(); // still prints "Right" dynamic_cast<L *>(br) -> f(); // prints "Left" bl -> f(); // prints "Left"; static_cast<R *>(bl) -> f(); // still prints "Left" dynamic_cast<R *>(bl) -> f(); // prints "Right"
const_cast
const_cast
- единственное приведение, которое можно использовать для добавления const
к типу или удаления const
из типа. Это полезно, если, скажем, вы хотите передать не const
аргумент функции, которая ожидает const
аргументов.
Рассмотрим эту функцию -
void f(int& x) { x = 5; }
Ожидается не const
ссылка на int
.
Теперь предположим, что у вас есть что-то вроде этого -
int i = 4; // non const variable const int& j = i; // const reference
Если вы попытаетесь вызвать f
с j
в качестве аргумента, вы получите сообщение об ошибке:
error: binding reference of type 'int&' to 'const int' discards qualifiers
Что вы можете сделать, так это удалить const
с помощью const_cast
—
f(const_cast<int&>(j));
И это сработает.
Однако обратите внимание, что вы можете удалить const
из объекта, только если он действительно был объявлен как не const
. Удаление const
из объекта const
- неопределенное поведение -
const int i= 3; // j is declared const int* p = const_cast<int*>(&i); *p = 4; // undefined behavior
Точно так же вы можете добавить к объекту const
. const_cast
работает и с изменчивым ключевым словом, хотя это бывает редко.
reinterpret_cast
Это наиболее опасная гипсовая повязка, и использовать ее следует с осторожностью. Не используйте его, если не знаете, что делаете. Это приведение преобразует любой тип указателя в любой другой тип указателя, даже несвязанные типы. Никаких проверок не производится. Он просто копирует двоичные данные из одного указателя в другой.
Разрешены все типы преобразований указателей. Вы даже можете приводить указатели к целочисленным типам и обратно. Единственная гарантия состоит в том, что если вы приведете указатель к целочисленному типу, который достаточно велик, чтобы удерживать его, а затем вернули его к указателю, вы получите действительный указатель.
Мы не будем вдаваться в подробности о reinterpret_cast
, по крайней мере, в этом посте. Если интересно, можете прочитать здесь.
Приведения в стиле C и функциональном стиле
И, наконец, у нас есть приведение в стиле C и стиле функции -
(type) object; // c-style cast type(object); // function-style cast
Эти два функционально эквивалентны и выполняют следующие действия в указанном порядке, пока один из них не добьется успеха:
const_cast
static_cast
(без учета ограничений доступа)static_cast
, затемconst_cast
reinterpret_cast
reinterpret_cast
, затемconst_cast
Лучше не использовать эти два из-за того, что они могут вызывать reinterpret_cast
, если вы не на 100% уверены, что static_cast
будет успешным. Даже в этом случае лучше явно использовать static_cast
.
Итак, теперь вы знаете о различных типах литья, предоставляемых C ++. Если вы хотите, вы можете прочитать о них подробно из следующих источников -