6

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

10

Я немного запутался в применении 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 ответ(ов)

5

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.

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;

    // сохраняем глобальные данные
    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, не подойдут.

0

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

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

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

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

0

Одним из случаев использования 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.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь