Когда использовать 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 вернуть оригинальный указатель.

Другие вопросы по тегам