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

460

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

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

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

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

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

HeretoLearn
источник
9
reinterpret_castне происходит во время выполнения. Они оба являются инструкциями времени компиляции. От en.cppreference.com/w/cpp/language/reinterpret_cast : «В отличие от static_cast, но, как и const_cast, выражение reinterpret_cast не компилируется ни в какие инструкции процессора. Это чисто директива компилятора, которая инструктирует компилятор обрабатывать последовательность битов (представление объекта) выражения, как если бы оно имело тип new_type. "
Cris Luengo
@ HeretoLearn, возможно ли добавить соответствующие фрагменты кода из файла * .c и * .cpp? Я думаю, что это может улучшить изложение вопроса.
ОренИшШалом

Ответы:

443

Стандарт 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должно быть предпочтительным.

jalf
источник
18
Мне нравится тот факт, что «б» не определено. Это мешает тебе делать глупости. Если вы приводите что-то к другому типу указателя, вы просите о проблемах, и тот факт, что вы не можете зависеть от этого, делает вас более осторожным. Если вы использовали static_cast <> выше, что в любом случае означает «b»?
Мартин Йорк,
3
Я думал, что reinterpret_cast <> гарантирует тот же битовый шаблон. (который не совпадает с действительным указателем на другой тип).
Мартин Йорк,
37
значение bбольше не является неопределенным в C ++ 11 при использовании reinterpret_cast. И в C ++ 03 слепок int*к void*было запрещено делать с reinterpret_cast(хотя составители не выполнили это , и это было непрактично, поэтому было изменено на C ++ 11).
Йоханнес Шауб -
55
Это на самом деле не отвечает на вопрос «когда использовать reinterpret_cast».
einpoklum
6
@LokiAstari Я думаю, что неопределенное не мешает вам делать глупости. Это остановит вас, только когда вы вспомните, что это не указано. Огромная разница. Лично я не люблю неуказанные. Слишком много, чтобы помнить.
Хелин Ван
158

Один случай, когда 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; }
jwfearn
источник
7
Да, это единственное значимое использование reinterpret_cast, о котором я могу подумать.
jalf
8
Это может быть поздний вопрос, но почему API поставщика не использует void*для этого?
Xeo
19
@Xeo Они не используют void *, потому что тогда они теряют (некоторую) проверку типов во время компиляции.
Иисус
4
Практический случай использования «непрозрачных» типов данных - это когда вы хотите представить API для C, но написать реализацию на C ++. ICU является примером библиотеки, которая делает это в нескольких местах. Например, в API проверки фальсификации вы имеете дело с указателями типа USpoofChecker*, где USpoofCheckerпустая структура. Однако под капотом, когда вы передаете a USpoofChecker*, он подвергается reinterpret_castвнутреннему типу C ++.
Sffc
@sffc, почему бы не предоставить тип структуры C пользователю?
Гупта
101

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

РЕДАКТИРОВАТЬ:

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

jaskmar
источник
1
Хороший пример! Я бы заменил short на uint16_t и unsigned char на uint8_t, чтобы сделать его менее понятным для человека.
Jan Turoň
@ JanTuroň правда, мы не можем предположить, что shortзанимает 16 бит в памяти. Исправлено.
jaskmar
1
Пример неверный. reinterpret_cast не разрешен в функциях constexpr
Майкл Векслер,
1
Прежде всего, этот код отклонен как последним clang (7.0.0), так и gcc (8.2.0). К сожалению, я не нашел ограничения в формальном языке. Все, что я мог найти, было social.msdn.microsoft.com/Forums/vstudio/en-US/…
Майкл Векслер
2
Более конкретно, en.cppreference.com/w/cpp/language/constant_expression (пункт 16) четко указывает, что reinterpret_cast нельзя использовать в константном выражении. Также посмотрите на github.com/cplusplus/draft/blob/master/papers/N3797.pdf (5.19 константных выражений) страниц 125-126, которые явно исключают reinterpret_cast. Затем 7.1.5 Элемент 5 спецификатора constexpr (стр. 146) * Для не шаблонной, не дефолтной функции constexpr ... если не существует значений аргумента, которые ... могли бы быть оцененным подвыражением выражения основной константы (5.19) ), программа плохо сформирована *
Майкл Векслер
20

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

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

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

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

Флодин
источник
5
Не совсем сбой вашей программы. Стандарт предлагает несколько гарантий о reinterpret_cast. Просто не так много, как люди часто ожидают.
jalf
1
Нет, если вы используете это правильно. То есть reinterpret_cast от A до B к A совершенно безопасен и четко определен. Но значение B не определено, и да, если вы полагаетесь на это, могут случиться плохие вещи. Но сам актерский состав достаточно безопасен, если вы используете его только так, как позволяет стандарт. ;)
Джалф
55
LOL, я подозреваю, что reinterpret_crash действительно может привести к сбою вашей программы. Но reinterpret_cast не будет. ;)
Джалф
5
<Ирония> Я попробовал это на моем компиляторе, и почему-то он отказался от компиляции reinterpret_crash. Ни в коем случае ошибка компилятора не помешает мне разбить мою программу интерпретации. Я
сообщу
18
@paercebaltemplate<class T, U> T reinterpret_crash(U a) { return *(T*)nullptr; }
12

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

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;                       // 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.

Адам П. Гоше
источник
1
error: invalid cast of an rvalue expression of type 'int64_t {aka long long int}' to type 'double&' reinterpret_cast<double&>((reinterpret_cast<int64_t&>(d) >> 1) + (1L << 61))- ideone.com/6S4ijc
Орвеллофил
1
Стандарт говорит, что это неопределенное поведение: en.cppreference.com/w/cpp/language/reinterpret_cast (в разделе «псевдоним типа»)
Cris Luengo
@CrisLuengo Если я заменю все reinterpret_castна memcpy, это все еще UB?
песчаная терна
@sandthorn: это UB в соответствии со стандартом, но если он работает для вашей архитектуры, не беспокойтесь об этом. Я полагаю, этот трюк подходит для любого компилятора для архитектур Intel. Он не мог работать должным образом (или даже падать) на других архитектурах - например, возможно, что плавающие и длинные значения хранятся в отдельных отсеках памяти (не то, чтобы я знал о любой такой архитектуре, это просто аргумент ...) , memcpyопределенно сделает это законным.
Крис Луенго
2
template <class outType, class inType>
outType safe_cast(inType pointer)
{
    void* temp = static_cast<void*>(pointer);
    return static_cast<outType>(temp);
}

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

Саша Зезулинский
источник
1
Какая? Зачем беспокоиться? Это именно то, что reinterpret_castуже происходит в этой ситуации: «Указатель объекта может быть явно преобразован в указатель объекта другого типа. [72] Когда значение v типа указателя объекта преобразуется в тип указателя объекта« указатель на cv T », результат static_cast<cv T*>(static_cast<cv void*>(v))". - N3797.
underscore_d
Что касается c++2003стандарта я НЕ считаю , что reinterpret_castделаетstatic_cast<cv T*>(static_cast<cv void*>(v))
Саша Zezulinsky
1
Хорошо, правда, но мне не важна версия от 13 лет назад, и большинство кодировщиков не должно этого делать, если (как это вероятно) они могут ее избежать. Ответы и комментарии должны действительно отражать последний доступный Стандарт, если не указано иное ... ИМХО. Во всяком случае, я полагаю, что Комитет счел необходимым явно добавить это после 2003 года. (Потому что IIRC, то же самое было и в C ++ 11)
underscore_d
До C++03этого было C++98. Тонны проектов использовали старый C ++ вместо переносимого C. Иногда вам нужно заботиться о переносимости. Например, вы должны поддерживать один и тот же код в Solaris, AIX, HPUX, Windows. Что касается зависимости и переносимости компилятора, то это сложно. Так что хороший пример введения ада переносимости - использование reinterpret_castв вашем коде
Саша Зезулинский
Опять же, если вы, как и я, будете рады ограничиться только платформами, которые хорошо работают с последней и лучшей версией языка, ваше возражение является спорным.
underscore_d
1

Сначала у вас есть данные определенного типа, например, 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__; })
cmdLP
источник
Это правда, что «это означает, что одна и та же память используется как другой тип», но она ограничена определенной парой типов. В вашем примере reinterpret_castформа intto float&является неопределенным поведением.
Jaskmar
1

Одна из причин использования 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
разница = 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 все еще не возвращает .

Ави Гинзбург
источник
1

Вот вариант программы Ави Гинзбурга, который ясно иллюстрирует свойство, 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);

TRPh
источник
-6

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

Мариус К
источник
-16

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

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

dirkgently
источник