Когда использовать reinterpret_cast?

Меня немного смущает применимость reinterpret_cast против static_cast. Из того, что я прочитал, общие правила заключаются в использовании статического приведения, когда типы могут интерпретироваться во время компиляции, отсюда и слово static. Это приведение, которое компилятор C++ использует внутренне для неявных приведений.

reinterpret_cast применимы в двух сценариях:

  • конвертировать целочисленные типы в типы указателей и наоборот
  • конвертировать один тип указателя в другой. Общая идея, которую я получаю, заключается в том, что это непереносимо и этого следует избегать.

Там, где я немного запутался, это одно использование, которое мне нужно, я вызываю C++ из C, а код C должен удерживать объект C++, поэтому в основном он содержит void*. Какое приведение следует использовать для преобразования между void * и типом класса?

Я видел использование обоих static_cast и reinterpret_cast? Хотя из того, что я читал, кажется, что static лучше, поскольку приведение может произойти во время компиляции? Хотя он говорит использовать reinterpret_cast для преобразования из одного типа указателя в другой?

Ответ 1

Стандарт 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, но тот, который не указан в стандарте, и это может быть неверно на машинах с более сложными системами памяти.)

Для приведения в и из void*, static_cast должен быть предпочтительным.

Ответ 2

Один случай, когда 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; }

Ответ 3

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

EDIT:

В первой версии я сделал пример функции is_little_endian для constexpr. Он прекрасно компилируется на новейшем gcc (8.3.0), но стандарт говорит, что это незаконно. Компилятор clang отказывается компилировать его (что правильно).

Ответ 4

Значение reinterpret_cast не определяется стандартом С++. Следовательно, теоретически a reinterpret_cast может привести к сбою вашей программы. На практике компиляторы стараются делать то, что вы ожидаете, что должно интерпретировать биты того, что вы проходите, как если бы это был тип, на который вы производите. Если вы знаете, что компиляторы, которые вы собираетесь использовать, используйте с reinterpret_cast, вы можете использовать его, но сказать, что он переносимый будет лгать.

В описанном вами случае и в любом случае, когда вы можете рассмотреть reinterpret_cast, вы можете использовать static_cast или другую альтернативу. Среди прочего, стандарт имеет это, чтобы сказать о том, чего вы можете ожидать от static_cast (§5.2.9):

Значение типа "указатель на cv void" может быть явно преобразован в указатель на тип объекта. Значение типа указатель на объект, преобразованный в "указатель на cv void" и обратно к исходному типу указателя, будет иметь свое исходное значение.

Итак, для вашего случая использования кажется довольно очевидным, что комитет по стандартизации предназначен для использования static_cast.

Ответ 5

Одно использование reinterpret_cast - это если вы хотите применять побитовые операции для (IEEE 754) поплавков. Одним из примеров этого был быстрый обратный квадрат-корневой трюк:

https://en.wikipedia.org/wiki/Fast_inverse_square_root#Overview_of_the_code

Он обрабатывает двоичное представление float как целое число, сдвигает его вправо и вычитает из константы, тем самым уменьшая наполовину и отрицая показатель экспоненты. После преобразования обратно в поплавок он подвергается итерации Ньютона-Рафсона, чтобы сделать это приближение более точным:

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 cast, но аналогичным С++ cast является reinterpret_cast.

Ответ 7

template <class outType, class inType>
outType safe_cast(inType pointer)
{
    void* temp = static_cast<void*>(pointer);
    return static_cast<outType>(temp);
}

Я попытался заключить и написал простой безопасный листинг с использованием шаблонов. Обратите внимание, что это решение не гарантирует бросать указатели на функции.

Ответ 8

Сначала у вас есть данные определенного типа типа 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

BRIEF: это означает, что одна и та же память используется как другой тип. Таким образом, вы можете преобразовать двоичные представления float как типа int, как указано выше, в float. Например, 0x80000000 имеет значение -0 (мантисса и показатель экспоненты равны нулю, но знак msb равен единице. Это также работает для удвоений и длинных удвоений.

ОПТИМИЗАЦИЯ: Я думаю, что reinterpret_cast будет оптимизирован во многих компиляторах, а c-casting будет выполняться с помощью pointerarithmetic (значение должно быть скопировано в память, поскольку указатели не могут указывать на cpu- регистраторы).

ПРИМЕЧАНИЕ. В обоих случаях вы должны сохранить значение casted в переменной перед произведением! Этот макрос может помочь:

#define asvar(x) ({decltype(x) __tmp__ = (x); __tmp__; })

Ответ 9

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

В качестве примера возьмем приведенный ниже код:

#include <cstdio>

class A {
public:
    int i;
};

class B : public A {
public:
    virtual void func() {  }
};

int main()
{
    B b;
    const A* a = static_cast<A*>(&b);
    const A* ar = reinterpret_cast<A*>(&b);

    printf("&b = %p\n", &b);
    printf(" a = %p\n", a);
    printf("ar = %p\n", ar);
    printf("difference = %ld\n", (long int)(a - ar));

    return 0;
}

Который выводит что-то вроде:

&b = 0x7ffe10e68b38
a = 0x7ffe10e68b40
ar = 0x7ffe10e68b38
difference = 2

Во всех компиляторах, которые я пробовал (MSVC 2015 и 2017, clang 8.0.0, gcc 9.2, icc 19.0.1 - см. Godbolt для последних 3), результат static_cast отличается от результата reinterpret_cast на 2 (4 для MSVC). Единственный компилятор, который предупредил о разнице, был clang с:

17:16: предупреждение: 'reinterpret_cast' из класса 'B *' в его базу с ненулевым смещением 'A *' ведет себя иначе, чем 'static_cast' [-Wreinterpret-base-class]
        const A * ar = reinterpret_cast (& b);
                      ^ ~~~~~~~~~~~~~~~~~~~~~~~
17:16: примечание: используйте static_cast для правильной настройки указателя во время апскейтинга
        const A * ar = reinterpret_cast (& b);
                      ^ ~~~~~~~~~~~~~~~
                      static_cast

Последнее предостережение заключается в том, что если базовый класс не имеет членов данных (например, int i;), то clang, gcc и icc возвращают тот же адрес для reinterpret_cast, что и для static_cast, тогда как MSVC все еще не делает.

Ответ 10

Быстрый ответ: используйте static_cast, если он компилируется, в противном случае обратитесь к reinterpret_cast.

Ответ 11

Никогда. Вы никогда не должны использовать его когда-либо. Вы можете использовать static_cast для преобразования из void* (здесь - демонстрация). Никогда НИКОГДА не используйте reinterpret_cast.

Он предназначен для доступа к аппаратным средствам. Вы пишете драйвер устройства? Я почти уверен, что вы этого не сделали. Поэтому писать ниже - это то, что вы не будете делать. Вы можете хранить указатели на int достаточно большие, чтобы удерживать его, и все мы знаем, что мы можем отображать int пользователю или выгружать его в файл для диагностики. Так что, возможно, для отладки арифметики указателя или сравнения их с другим, но это тоже вряд ли.

#define pHardwareRegName reinterpret_cast<volatile int*>(0x11223344)
//later in code
hardwareRegName = *pHardwareRegName; //remember hardwareRegName is not volatile

Ответ 12

Прочитайте FAQ! Удержание данных С++ на C может быть рискованным.

В С++ указатель на объект может быть преобразован в void * без каких-либо бросков. Но это не так. Вам понадобится static_cast, чтобы вернуть исходный указатель.