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 ++ предлагает несколько видов оружия -

  1. static_cast
  2. dynamic_cast
  3. const_cast
  4. reinterpret_cast
  5. Приведение стиля 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 ++. Если вы хотите, вы можете прочитать о них подробно из следующих источников -

  1. Обзор кастинга
  2. Static_cast
  3. Reinterpret_cast
  4. Const_cast
  5. Dynamic_cast