Когда использовать reinterpret_cast?
Я немного запутался с применимостью reinterpret_cast
против static_cast
, Из того, что я прочитал, общие правила - использовать статическое приведение, когда типы могут интерпретироваться во время компиляции, отсюда и слово static
, Это приведение, которое компилятор C++ использует внутренне для неявных приведений.
reinterpret_cast
Они применимы в двух сценариях, преобразуют целочисленные типы в типы указателей и наоборот или для преобразования одного типа указателя в другой. Общая идея, которую я получаю, заключается в том, что это непереносимо и этого следует избегать.
Там, где я немного запутался, это одно использование, которое мне нужно, я вызываю C++ из C, а код C должен поддерживать объект C++, поэтому в основном он содержит void*
, Какой актерский состав следует использовать для преобразования между void *
а тип класса?
Я видел использование обоих static_cast
а также reinterpret_cast
? Хотя из того, что я читал, похоже, static
лучше, как приведение может произойти во время компиляции? Хотя это говорит, чтобы использовать reinterpret_cast
преобразовать из одного типа указателя в другой?
10 ответов
Стандарт 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
только гарантирует, что если вы приведете указатель на другой тип, а затем reinterpret_cast
обратно к исходному типу, вы получите исходное значение. Итак, в следующем:
int* a = new int();
void* b = reinterpret_cast<void*>(a);
int* c = reinterpret_cast<int*>(b);
a и c содержат одно и то же значение, но значение b не указано. (на практике он обычно содержит тот же адрес, что и a и c, но это не указано в стандарте, и это может быть неверно на машинах с более сложными системами памяти.)
Для приведения в и из пустоты *, 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;
// store global data
VendorGlobalUserData d1;
// d1 = &u; // compile error
// d1 = static_cast<VendorGlobalUserData>(&u); // compile error
d1 = reinterpret_cast<VendorGlobalUserData>(&u); // ok
VendorSetUserData(d1);
// do other stuff...
// retrieve global data
VendorGlobalUserData d2 = VendorGetUserData();
MyUserData * p = 0;
// p = d2; // compile error
// p = static_cast<MyUserData *>(d2); // compile error
p = reinterpret_cast<MyUserData *>(d2); // ok
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
означает, не используйте его. Если вам это понадобится в будущем, вы будете знать.
Полный ответ:
Давайте рассмотрим основные типы чисел.
Когда вы конвертируете, например, int(12)
в unsigned float (12.0f)
Ваш процессор должен вызвать некоторые вычисления, так как оба числа имеют разное представление битов. Это то, что static_cast
обозначает.
С другой стороны, когда вы звоните reinterpret_cast
ЦП не вызывает никаких вычислений. Он просто обрабатывает набор битов в памяти, как если бы он имел другой тип. Поэтому, когда вы конвертируете int*
в float*
с этим ключевым словом новое значение (после разыменования указателя) не имеет ничего общего со старым значением в математическом смысле.
Пример: это правда, что reinterpret_cast
не является переносимым по одной причине - порядку байтов (порядку байтов). Но это часто удивительно лучшая причина для его использования. Давайте представим пример: вы должны прочитать двоичное 32-битное число из файла, и вы знаете, что это порядковый номер. Ваш код должен быть универсальным и работать должным образом в системах с прямым порядком байтов (например, ARM) и с прямым порядком байтов (например, x86). Таким образом, вы должны проверить порядок байтов. Это хорошо известно во время компиляции, так что вы можете написать constexpr
функция:
constexpr bool is_little_endian() {
std::uint16_t x=0x0001;
auto p = reinterpret_cast<std::uint8_t*>(&x);
return *p != 0;
}
Пояснение: двоичное представление x
в памяти может быть 0000'0000'0000'0001
(большой) или 0000'0001'0000'0000
(немного порядковый номер). После переинтерпретации-приведения байта под p
указатель может быть соответственно 0000'0000
или же 0000'0001
, Если вы используете статическое приведение, оно всегда будет 0000'0001
, независимо от того, какой порядок байтов используется.
Значение reinterpret_cast
не определяется стандартом C++. Следовательно, в теории reinterpret_cast
может привести к сбою вашей программы. На практике компиляторы пытаются делать то, что вы ожидаете, то есть интерпретировать биты того, что вы передаете, как если бы они были типом, к которому вы приводите. Если вы знаете, что делать с компиляторами, которые вы собираетесь использовать reinterpret_cast
Вы можете использовать это, но сказать, что это портативно, было бы ложью.
Для случая, который вы описываете, и в значительной степени любой случай, когда вы могли бы рассмотреть reinterpret_cast
, ты можешь использовать static_cast
или какая-то другая альтернатива. Помимо всего прочего, в стандарте сказано, что вы можете ожидать от static_cast
(§5.2.9):
Значение типа "указатель на cv void" может быть явно преобразовано в указатель на тип объекта. Значение указателя типа на объект, преобразованное в "указатель на cv void" и обратно в исходный тип указателя, будет иметь свое первоначальное значение.
Таким образом, для вашего случая использования довольно ясно, что комитет по стандартизации предназначен для вас, чтобы использовать static_cast
,
Одно из применений reinterpret_cast - это если вы хотите применять побитовые операции к (IEEE 754) числам с плавающей точкой. Одним из примеров этого был трюк Fast Inverse Square-Root:
https://en.wikipedia.org/wiki/Fast_inverse_square_root
Он рассматривает двоичное представление числа с плавающей точкой как целое число, сдвигает его вправо и вычитает его из константы, тем самым вдвое уменьшая и отрицая показатель степени. После преобразования обратно в число с плавающей точкой он подвергается итерации Ньютона-Рафсона, чтобы сделать это приближение более точным:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the deuce?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
Первоначально он был написан на C, поэтому использует приведение C, но аналогичным приведением C++ является reinterpret_cast.
Вот вариант программы Ави Гинзбурга, наглядно демонстрирующий свойство reinterpret_cast
упомянутые Крисом Луенго, flodin и cmdLP: компилятор обрабатывает указанную ячейку памяти, как если бы это был объект нового типа:
#include <iostream>
#include <string>
#include <iomanip>
using namespace std;
class A
{
public:
int i;
};
class B : public A
{
public:
virtual void f() {}
};
int main()
{
string s;
B b;
b.i = 0;
A* as = static_cast<A*>(&b);
A* ar = reinterpret_cast<A*>(&b);
B* c = reinterpret_cast<B*>(ar);
cout << "as->i = " << hex << setfill('0') << as->i << "\n";
cout << "ar->i = " << ar->i << "\n";
cout << "b.i = " << b.i << "\n";
cout << "c->i = " << c->i << "\n";
cout << "\n";
cout << "&(as->i) = " << &(as->i) << "\n";
cout << "&(ar->i) = " << &(ar->i) << "\n";
cout << "&(b.i) = " << &(b.i) << "\n";
cout << "&(c->i) = " << &(c->i) << "\n";
cout << "\n";
cout << "&b = " << &b << "\n";
cout << "as = " << as << "\n";
cout << "ar = " << ar << "\n";
cout << "c = " << c << "\n";
cout << "Press ENTER to exit.\n";
getline(cin,s);
}
В результате получается такой вывод:
as->i = 0
ar->i = 50ee64
b.i = 0
c->i = 0
&(as->i) = 00EFF978
&(ar->i) = 00EFF974
&(b.i) = 00EFF978
&(c->i) = 00EFF978
&b = 00EFF974
as = 00EFF978
ar = 00EFF974
c = 00EFF974
Press ENTER to exit.
Можно видеть, что объект B встраивается в память сначала как данные, специфичные для B, а затем - внедренный объект A. Вstatic_cast
правильно возвращает адрес встроенного объекта A, а указатель, созданный static_cast
правильно дает значение поля данных. Указатель, созданныйreinterpret_cast
лечит b
место в памяти, как если бы это был простой объект A, и поэтому, когда указатель пытается получить поле данных, он возвращает некоторые данные, специфичные для B, как если бы это было содержимое этого поля.
Одно использование reinterpret_cast
заключается в преобразовании указателя в целое число без знака (когда указатели и целые числа без знака имеют одинаковый размер):
int i;
unsigned int u = reinterpret_cast<unsigned int>(&i);
Вы можете использовать reinterprete_cast для проверки наследования во время компиляции.
Посмотрите здесь: Использование reinterpret_cast для проверки наследования во время компиляции
Сначала у вас есть данные определенного типа, например, int:
int x = 0x7fffffff://==nan in binary representation
Затем вы хотите получить доступ к той же переменной, что и другой тип, такой как float: вы можете выбрать между
float y = reinterpret_cast<float&>(x);
//this could only be used in cpp, looks like a function with template-parameters
или же
float y = *(float*)&(x);
//this could be used in c and cpp
КРАТКОЕ ОПИСАНИЕ: это означает, что одна и та же память используется как другой тип. Таким образом, вы можете преобразовать двоичные представления типов с плавающей точкой, как указано выше, в типы с плавающей точкой. Например, 0x80000000 - это -0 (мантисса и показатель степени равны нулю, но знак msb равен единице. Это также работает для двойных и длинных двойных чисел.
ОПТИМИЗАЦИЯ: Я думаю, что reinterpret_cast будет оптимизирован во многих компиляторах, в то время как c-приведение выполняется с помощью pointerarithmetic (значение должно быть скопировано в память, потому что указатели не могут указывать на cpu-регистры).
ПРИМЕЧАНИЕ: в обоих случаях вы должны сохранить приведенное значение в переменной перед приведением! Этот макрос может помочь:
#define asvar(x) ({decltype(x) __tmp__ = (x); __tmp__; })
template <class outType, class inType>
outType safe_cast(inType pointer)
{
void* temp = static_cast<void*>(pointer);
return static_cast<outType>(temp);
}
Я попытался заключить и написал простое безопасное приведение, используя шаблоны. Обратите внимание, что это решение не гарантирует наведение указателей на функции.
Быстрый ответ: используйте static_cast
если он компилируется, в противном случае прибегают к reinterpret_cast
,
Прочитайте FAQ! Хранение данных C++ в C может быть рискованным.
В C++ указатель на объект может быть преобразован в void *
без каких-либо забросов. Но это не так, наоборот. Тебе нужен static_cast
вернуть оригинальный указатель.