Когда использовать C-style касты?

Рейтинг: 4Ответов: 2Опубликовано: 22.06.2023

В C++ есть куча своих кастов (static_cast, dynamic_cast, ...). Есть ли случаи, когда стоит использовать C-style каст (type)obj?

Ответы

▲ 5Принят

Сначала терминология:

  • MyType(x) - это functional cast. От C-style каста он ничем не отличается1, поэтому ниже я и тот и другой буду называть C-style кастом.

  • MyType(x, y, ...) - это вызов конструктора. Это не C-style каст.

  • MyType{x} и MyType{x, y, ...} - это не C-style касты.


Есть два (с половиной) лагеря. (Относительно разумных лагеря. Вообще везде писать C-style касты неразумно, поэтому не рассматривается.)

Лагерь 1: C-style касты пишутся короче, поэтому там, где они безопасны, можно/лучше их и использовать.

Основная претензия к C-style кастам в их работе с указателями и ссылками.

(MyClass &)ptr - вот это что? Варианты:

  • Каст к родителю (который может сдвигать указатель при множественном наследовании).

    • Вообще каст к непубличному (точнее, недоступному) родителю, который C++-style касты не способны выполнить.
  • reinterpret_cast.

  • Гнусный const_cast (возможно в комбинации с reinterpret_cast).

  • operator MyClass & у класса.

(аналогично каст в ссылку, не считая последнего пункта)

Во-первых, на глаз сложно определить, какой из вариантов тут сработал. Во-вторых, вариант может неожиданно поменяться при изменении кода (убрали родителя, и вполне законный каст к родителю стал работать как reinterpret_cast).

Еще возможная проблема - хотели сделать reinterpret_cast, но ошиблись типом так, что добавился еще и const_cast. Или наоборот.

А касты в не-указатель-не-ссылку вполне безобидны:

void DrawHealthBar(float percentage); // 0 <= percentage <= 1

int current_health = 80;
int max_health = 100;
DrawHealthBar(current_health / float(max_health));

От того, что вы здесь напишете static_cast<float>(max_health), код безопаснее не станет.

Поэтому лагерь 1 считает, что к не указателям и не ссылкам можно кастовать C-style кастом, а к остальному нужно кастовать C++-кастами.

Тут возможны уточнения, например (T *)nullptr от плюсового каста никак не выигрывает. Можно либо разрешить для краткости, либо запретить для упрощения правил.

Лагерь 2: Везде C++-style касты для однообразия.

Не "для безопасности", как иногда говорят, потому что это ничем не безопаснее варианта 1.

Это именно упрощение правил, чтобы не надо было запоминать, когда С-style касты безопасны, а когда нет. Но это происходит за счет удлиннения записи.


Безотносительно двух лагерей еще есть MyClass{x}, который еще более ограничен, чем C-style cast и static_cast. (Например, int во float не кастует, потому что потеря точности.)

В обоих лагерях некоторые любят по умолчанию использовать его вместо C-style каста и static_castа, если он работает. А можно сразу брать C-style каст или static_cast.

В тему: MyClass x(...); против MyClass x{...};.


1 Одно небольшое отличие - functional cast не компилируется для некоторых типов, в записи которых испольуются пробелы или специальные символы. Это просто ограничение синтаксиса, и если вынести тип в typedef/using, то все начинает работать:

// int *ptr = int*(x); // Ошибка.
// unsigned int y = unsigned int(x); // Ошибка. Хотя MSVC это компилирует, это нарушает стандарт.
using A = int *;
int *ptr = A(x); // ок
using B = unsigned int;
unsigned int y = B(x); // ок
unsigned int z = unsigned(x); // ок
▲ 2

Я бы использовал C-style касты в следующих случаях:

  1. когда необходим игнорирующий access specifiers каст:
struct A {
};

struct B : private A {
}* b;

// auto c = static_cast<A*>(b); // CE
auto c = (A*)b; // OK

Это возможно только с помощью С-style каста и используется в реальном коде (см. P2637).

[expr.cast]/4:

The conversions performed by

— a const_­cast,

— a static_­cast,

— a static_­cast followed by a const_­cast,

— a reinterpret_­cast, or

— a reinterpret_­cast followed by a const_­cast,

can be performed using the cast notation of explicit type conversion. The same semantic restrictions and behaviors apply, with the exception that in performing a static_­cast in the following situations the conversion is valid even if the base class is inaccessible: ...

  1. когда хотим проигнорировать возвращаемое значение функции и не можем/хотим использовать другие решения:
// external function without [[maybe_unused]]
bool foo_with_side_effects();

// too verbose
[[maybe_unused]] auto _ = foo_with_side_effects();
static_cast<void>(foo_with_side_effects());

// unspecified and requires a header
#include <tuple>
std::ignore = foo_with_side_effects();

// requires P2169 in C++26
auto _ = foo_with_side_effects();

// good ol' C-style cast
(void)foo_with_side_effects();