Избавьтесь от концепций актерского состава C++ с помощью «Звездных войн»

Приведение типов в C++ — распространенный камень преткновения для разработчиков. В чем разница между static_cast и dynamic_cast? Или приведение в стиле C против std::move? При каких обстоятельствах вам даже нужно использовать const_cast?

К счастью, Хан Соло предлагает удобную рубрику, позволяющую распутать множество нитей приведения C++.

Базовый класс

Мы начнем с базового класса, который будет представлен Ханом Соло в образе Харрисона Форда в оригинальных Звездных войнах 1977 года:

class HanSolo {};

class HarrisonFord : public HanSolo {
 private:
  int actor_;
};

HanSolo* han_solo = new HarrisonFord();

Все идет нормально.

static_cast

Самый простой актерский состав — static_cast, представленный Ханом Соло в роли Харрисона Форда в фильме 2015 года Звездные войны: Пробуждение силы.

Он тот же актер, только более опытный, так что можно с уверенностью играть актерский состав между этими двумя Ханами Соло, которые на самом деле являются одним и тем же Харрисоном Фордом (хотя это может привести к неопределенному поведению, если мы попытаемся заменить более старого, более производного Харрисона Форда 2015 года на младший Харрисон Форд 1977 года выпуска):

class HanSolo {};

class HarrisonFord1977 : public HanSolo {
 private:
  int a_new_hope_;
};

class HarrisonFord2015 : public HarrisonFord1977 {
 private:
  int the_force_awakens_;
};

HanSolo* young_han = new HarrisonFord1977();
HanSolo* old_han = new HarrisonFord2015();

// This compiles but produces undefined behavior.
HarrisonFord2015* undefined_han = static_cast<HarrisonFord2015*>(young_han);
// This compiles and is safe.
HarrisonFord2015* old_han1 = static_cast<HarrisonFord2015*>(old_han);

Это static_cast. Он выполняет только проверку типов во время компиляции, чтобы убедиться, что два класса, участвующих в приведении, связаны между собой. Это позволяет понижать уровень до более производного типа и повышать уровень до менее производного типа.

Это допускает небезопасные преобразования, поскольку базовый класс будет преобразован в производный класс без проверки типа указателя.

dynamic_cast

На ступеньку выше static_cast стоит dynamic_cast, представленный Ханом Соло в роли Олдена Эренрайха в фильме 2018 года Соло: Звёздные войны: Истории. Он совершенно другой актер в побочном сериале, поэтому небезопасно просто менять одного Хана Соло на другого.

Нам нужно выполнить проверку типа во время выполнения, чтобы убедиться, что наши два Хана Соло совместимы, даже если кажется, что они «реализуют один и тот же интерфейс»:

class HanSolo {};

class HarrisonFord1977 : public HanSolo {
 private:
  int a_new_hope_;
};

class AldenEhrenreich2018 : public HanSolo {
 private:
  int a_star_wars_story_;
};

HanSolo* harrison = new HarrisonFord1977();
HanSolo* alden = new AldenEhrenreich2018();

// This compiles and produces a valid pointer.
HarrisonFord1977* young_han1 = static_cast<HarrisonFord1977*>(harrison);
// This compiles but returns `nullptr` instead.
HarrisonFord1977* young_han2 = static_cast<HarrisonFord1977*>(alden);

Это dynamic_cast. Он выполняет дополнительную проверку типа задействованных указателей во время выполнения, чтобы убедиться, что он не выполняет небезопасное приведение вниз, которое в противном случае было бы разрешено static_cast.

Используя это, мы можем быть уверены, что случайно не бросим Олдена Эренрайха на Харрисона Форда.

const_cast

Еще одна особенность C++ — число const_cast, которое представляет Хан Соло, застывший в карбоните из фильма 1980-х годов Star Wars V: The Empire Strikes Back. Хан все еще Хан, но он был заморожен, поэтому нам требуется const_cast, чтобы разморозить его обратно в его изменчивое состояние:

class HanSolo {};

class HarrisonFord : public HanSolo {
 public:
  int original_trilogy;

  void check_trilogy() const {
    // This line wouldn't compile since the method is `const`.
    // original_trilogy += 3;
  }

  void break_trilogy() const {
    HarrisonFord* unfrozen_hans = const_cast<HarrisonFord*>(this);
    unfrozen_hans->original_trilogy += 3;
  }
};

const HarrisonFord* carbonite_han_solo = new HarrisonFord();
carbonite_han_solo->break_trilogy();

Это const_cast. Ключевое слово const было добавлено в C++, чтобы обеспечить дополнительную оптимизацию компилятора и повысить производительность. const пути кода обычно могут быть более эффективными, чем изменяемые пути кода, и компилятор должен иметь возможность соглашаться на их использование, когда это возможно.

Но, поскольку это в основном элемент оптимизации, а не безопасности типов, const_cast позволяет вам убрать проверки безопасности и вызвать потенциально неопределенное поведение, когда захотите.

переинтерпретировать_cast

Воплощением актерского состава является reinterpret_cast, представленный Джаббой Хаттом из Звездных войн VI: Возвращение джедая 1983 года. Потому что Джабба Хатт никоим образом не является Ханом Соло — но вы все равно можете превратить его в одного из них, используя reinterpret_cast:

class HanSolo {};

class JabbaTheHutt {};

HanSolo* han1 = new HanSolo();
JabbaTheHutt* jabba1 = new JabbaTheHutt();

// All totally fine.
HanSolo* han2 = reinterpret_cast<HanSolo*>(jabba1);
JabbaTheHutt* jabba2 = reinterpret_cast<JabbaTheHutt*>(han1);

Это reinterpret_cast. Он говорит компилятору просто отбросить один тип и заменить его другим без каких-либо проверок. Возможно, вам это нужно, потому что вы знаете, что поведение, которое вы ищете, правильное, и это действительно одна из тех чрезвычайных ситуаций уровня «Разбить стекло в случае чрезвычайной ситуации», поэтому вы идете вперед и reinterpret_cast одно к другому.

Но если байты не выровнены так, как должны, это приведет к неопределенному поведению.

станд:: двигаться

Новым дополнением к C++, начиная с C++11 (и пересмотренным в C++14), является std::move, который представлен Ханом Соло, привязанным к косе и транспортируемым эвоками в Star Wars VI: Return of the 1983 года. Джедай. Здесь Хан Соло — тот же актер той же эпохи, просто его куда-то везут помимо его воли:

std::string han_solo = "Han Solo";
// Prints: "It's Han Solo"
std::cout << "It's " << han_solo << std::endl;

std::vector<std::string> ewoks;
ewoks.push_back(han_solo);
// Prints: "It's still Han Solo"
std::cout << "It's still " << han_solo << std::endl;

ewoks.push_back(std::move(han_solo));
// Prints: "He's gone! "
std::cout << "He's gone! " << han_solo << std::endl;

Это std::move. Когда у нас есть объект в одном месте, память которого мы хотим заменить в другом месте, std::move облегчит передачу, не вызывая никаких посторонних копий по пути.

Хотя все дело в эффективности, это может привести к неожиданному поведению, если программисты не привыкли к его иногда неочевидным побочным эффектам.

Краткое содержание

Когда вы впервые начинаете изучать приведения типов в C++, может показаться, что их так много с немного разными именами, с очень тонкими функциями и сомнительной безопасностью.

Но, как только вы получите немного больше опыта, вы поймете, что — все это действительно так, просто иногда небезопасное и неопределенное поведение — это именно то, что нужно для компиляции вашего кода.