Что такое преимущества использования этих операторов вместо неявного кастинга в C++?
dynamic_cast <new_type> (expression)
reinterpret_cast <new_type> (expression)
static_cast <new_type> (expression)
Да ведь где, в которой ситуации мы должны использовать их? И действительно ли это верно, что они редко используются в ООП?
Из приведенного вами списка кастов единственный, который имеет смысл использовать для замены неявного каста, это static_cast.
dynamic_cast используется для понижения суперкласса в его подкласс. Это не может произойти неявно, и на самом деле это не такая уж редкая вещь в ООП. static_cast тоже может быть использован в таком кастинге, однако он более опасен, поскольку не проверяет во время выполнения, что кастинг действителен.
Последний каст, reinterpret_cast, следует использовать очень осторожно, так как он является самым опасным из всех. По сути, с его помощью вы можете преобразовать что угодно во что угодно - но вам, как программисту, придется убедиться, что такое преобразование имеет смысл с семантической точки зрения, поскольку вы, по сути, отключаете проверку типов, выполняя такое преобразование.
Просто хочу добавить пример ситуации, в которой используется reinterpret_cast. Представьте, что библиотека дает вам указатель на необработанный сетевой пакет. Чтобы разобраться в структуре данных, вы можете использовать структуры, а затем приводить указатели к этим структурам. Обратите внимание, что у компилятора нет способа проверить, делаете ли вы здесь что-то разумное или просто собираетесь читать в памяти то, что не следует. В реальной ситуации вы бы сначала проверили размер пакета, чтобы убедиться, что он достаточно велик для размещения в нем ваших структур.
В приведенном ниже коде конструктор IPfragment получает пакет и затем преобразовывает указатель во что-то разумное. Я добавил определения ниже.
Если кто-то все еще думает, что использование reinterpret_cast неоправданно, я буду рад услышать о лучшем способе сделать это.
IPfragment::IPfragment( const byte* const pkt_data ) :
ethernetHeader( reinterpret_cast< const EthernetHeader* >( pkt_data ) )
, ipHeader ( reinterpret_cast< const IPheader* >( pkt_data + ETHER_HEADER_LEN ) )
, payload ( reinterpret_cast< const byte* >( ipHeader )
+ ( ipHeader->ver_hl & 0x0f ) *4 )
{
}
Вот определения:
typedef uint8_t byte ;
typedef uint16_t word ;
typedef uint32_t dword ;
#define ETHER_ADDR_LEN 6 // Ethernet addresses are 6 bytes
#define ETHER_HEADER_LEN 14 // Ethernet headers are 14 bytes
#define ETHER_TYPE_IP4 8
struct EthernetHeader
{
byte etherDestHost[ETHER_ADDR_LEN]; // Destination host address
byte etherSrcHost [ETHER_ADDR_LEN]; // Source host address
word etherType; // IP? ARP? RARP? etc
};
/* 4 bytes IP address */
struct IPaddress
{
byte byte1, byte2, byte3, byte4;
};
/* IPv4 header */
struct IPheader
{
byte ver_hl ; // Version (4 bits) + Internet header length (4 bits)
byte tos ; // Type of service
word tlen ; // Total length
word identification ; // Identification
word flags_fo ; // Flags (3 bits) + Fragment offset (13 bits)
byte ttl ; // Time to live
byte proto ; // Protocol
word crc ; // Header checksum
IPaddress saddr ; // Source address
IPaddress daddr ; // Destination address
dword op_pad ; // Option + Padding
};
class IPfragment
{
public:
const IPheader* const ipHeader;
const EthernetHeader* const ethernetHeader;
const byte* const payload;
IPfragment( const byte* const pkt_data );
// rest of code omitted for brevity
}
Для очень краткого ответа, преимущества этих приведений в том, что они выполняют конкретные функции, делая код описательным. Приведение в стиле C является всемогущим и позволяет избежать любых ошибок. Те, кто привык к языку C, могут пожаловаться, что касты - это мучение для написания кода. На самом деле другие считают это положительным моментом: это отбивает у программистов желание рассыпать касты по всему коду, что является очевидным признаком проблемного кода. Наконец, их легко найти с помощью текстового поиска.
Обычно я видел, что подобные приведения появляются в коде, когда что-то больше не собирается, возможно, потому что мы начали использовать новый компилятор, который более строг к неявным преобразованиям, так что это ключевое "преимущество" перед неявными преобразованиями. Очевидно, что правильнее всего в такой ситуации изменить код каким-то другим способом!
Dynamic_cast можно использовать для приведения "вверх по течению" с полиморфизмом. Так что если у вас есть структура вроде этой;
Base -> Derived A
Base -> Derived B
вы можете сделать dynamic_cast(b); (b - указатель на Base, но на самом деле это Derived_B) ;
Если это не класс Derived_B, то вместо преобразованного указателя вы получите 0.
Это намного медленнее, чем static_cast, поскольку проверка выполняется во время выполнения, а не во время компиляции, но предназначение у этого метода другое.
reinterpret_cast просто изменяет метку типа, позволяя использовать забавный FX в стиле C (или "type-punning", как его обычно называют), полезный для протоколов и низкоуровневой работы, но его следует использовать редко.
Обычно большое количество кастов в коде является признаком того, что что-то не так с дизайном вашего кода.
Как и любая другая неявная вещь, она может скрывать логику, которую разработчик / рецензент не имел в виду, маскируя ошибки.
Одним из преимуществ использования кастов C++ вместо кастов в стиле C является то, что их легко искать. Они также разделяют различные применения кастомов в стиле Си, делая запахи легко идентифицируемыми.
Например, grep для reinterpret_cast может легко найти множество потенциальных проблем и плохих конструкций, в то время как regex, правильно определяющий касты в стиле C, потребует дополнительной проверки для выявления плохих кастов.
Если приведение необходимо, я бы всегда использовал приведение в стиле C++ и никогда в стиле C.
См. стандарты кодирования C++, Саттер и Александреску, пункт 95.