Когда следует использовать reinterpret_cast?
Я немного запутался в применении reinterpret_cast
и static_cast
. Из того, что я прочитал, общие правила таковы: следует использовать static_cast
, когда типы могут быть интерпретированы на этапе компиляции, поэтому и слово static
. Это именно тот каст, который компилятор C++ использует внутренне для неявных преобразований.
reinterpret_cast
применим в двух сценариях:
- преобразование целочисленных типов в указательные и наоборот;
- преобразование одного указательного типа в другой. Общее представление, которое у меня есть, заключается в том, что это непортативно и должно быть избегаемо.
Где я немного запутался, так это в одном применении, которое мне нужно: я вызываю C++ из C, и код на C должен сохранять объект C++, поэтому, по сути, он хранит void*
. Какой каст следует использовать для преобразования между void*
и типом класса?
Я видел использование как static_cast
, так и reinterpret_cast
. Однако, из того, что я читал, кажется, что static_cast
лучше, так как преобразование может произойти на этапе компиляции. Однако говорят, что следует использовать reinterpret_cast
для преобразования из одного указательного типа в другой.
4 ответ(ов)
C++ стандарт гарантирует следующее:
static_cast
для преобразования указателя в тип void*
и обратно сохраняет адрес. Это означает, что в следующем примере переменные a
, b
и c
указывают на один и тот же адрес:
int* a = new int();
void* b = static_cast<void*>(a);
int* c = static_cast<int*>(b);
С другой стороны, reinterpret_cast
гарантирует только то, что если вы преобразуете указатель в другой тип, а затем снова преобразуете его в исходный тип, вы получите исходное значение. Таким образом, в следующем примере:
int* a = new int();
void* b = reinterpret_cast<void*>(a);
int* c = reinterpret_cast<int*>(b);
Переменные a
и c
содержат одно и то же значение, но значение b
не определено. (На практике оно обычно будет содержать тот же адрес, что и a
, и c
, но это не указано в стандарте, и может не сработать на машинах с более сложными системами памяти.)
Для преобразования в void*
и обратно следует предпочесть static_cast
.
В одном случае, когда необходимо использовать reinterpret_cast
, это связано с взаимодействием с непрозрачными типами данных. Такие случаи часто встречаются в API от сторонних производителей, над которыми программист не имеет контроля. Вот упрощенный пример, в котором поставщик предоставляет API для хранения и извлечения произвольных глобальных данных:
// vendor.hpp
typedef struct _Opaque * VendorGlobalUserData;
void VendorSetUserData(VendorGlobalUserData p);
VendorGlobalUserData VendorGetUserData();
Чтобы использовать этот API, программист должен преобразовать свои данные в VendorGlobalUserData
и обратно. static_cast
в этом случае не сработает, нужно использовать reinterpret_cast
:
// main.cpp
#include "vendor.hpp"
#include <iostream>
using namespace std;
struct MyUserData {
MyUserData() : m(42) {}
int m;
};
int main() {
MyUserData u;
// сохраняем глобальные данные
VendorGlobalUserData d1;
// d1 = &u; // ошибка компиляции
// d1 = static_cast<VendorGlobalUserData>(&u); // ошибка компиляции
d1 = reinterpret_cast<VendorGlobalUserData>(&u); // нормально
VendorSetUserData(d1);
// выполняем другие действия...
// извлекаем глобальные данные
VendorGlobalUserData d2 = VendorGetUserData();
MyUserData * p = 0;
// p = d2; // ошибка компиляции
// p = static_cast<MyUserData *>(d2); // ошибка компиляции
p = reinterpret_cast<MyUserData *>(d2); // нормально
if (p) { cout << p->m << endl; }
return 0;
}
Ниже приведена упрощенная реализация примерного API:
// vendor.cpp
static VendorGlobalUserData g = 0;
void VendorSetUserData(VendorGlobalUserData p) { g = p; }
VendorGlobalUserData VendorGetUserData() { return g; }
Таким образом, reinterpret_cast
необходим для работы с непрозрачными типами данных, предоставляемыми сторонними библиотеками, так как в таких случаях стандартные механизмы преобразования типов, такие как static_cast
, не подойдут.
Значение reinterpret_cast
не определено стандартом C++, поэтому теоретически его использование может привести к сбоям в вашей программе. На практике же компиляторы стараются делать то, что вы ожидаете, то есть интерпретировать биты того, что вы передаете, как если бы они были типом, в который вы приводите. Если вы знаете, как именно компиляторы, которые вы собираетесь использовать, обрабатывают reinterpret_cast
, вы можете его применять, но утверждать, что это портативно, было бы неверно.
Для вашего случая, а также для большинства случаев, где вы могли бы рассмотреть использование reinterpret_cast
, лучше использовать static_cast
или другой аналогичный подход. В стандарте, в частности, указано следующее о том, чего вы можете ожидать от static_cast
(§5.2.9):
Объект rvalue типа "указатель на cv void" может быть явно преобразован в указатель на объект. Значение типа указателя на объект, преобразованное в "указатель на cv void" и обратно в исходный тип указателя, вернет свое исходное значение.
Таким образом, для вашего случая кажется вполне очевидным, что стандартная комиссия подразумевала использование static_cast
.
Одним из случаев использования reinterpret_cast
является необходимость выполнять побитовые операции с числами с плавающей запятой (формат IEEE 754). Ярким примером такого применения является трюк с быстрой обратной квадратной коренной функцией:
https://en.wikipedia.org/wiki/Fast_inverse_square_root#Overview_of_the_code
В этом методе двоичное представление числа с плавающей запятой рассматривается как целое, затем оно сдвигается вправо и вычитается из константы, что, по сути, делит показатель на два и меняет его знак. После этого, преобразовав обратно в число с плавающей запятой, происходит итерация Ньютона-Рафсона для улучшения точности приближения:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // зло, манипуляции на уровне бит
i = 0x5f3759df - ( i >> 1 ); // что за чертовщина?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1-я итерация
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2-я итерация, это можно убрать
return y;
}
Этот код изначально написан на C, поэтому в нем используются приведения типов C, но аналогичный способ в C++ — это reinterpret_cast
.
Сравнение регулярного приведения типов, static_cast и dynamic_cast
Почему стоит использовать static_cast<T>(x) вместо (T)x?
Что такое Правило трёх?
Разница между const int*, const int * const и int * const?
Имеют ли круглые скобки после имени типа значение при использовании new?