Коли використовувати reinterpret_cast?


459

Я трохи плутають з применимостью reinterpret_castпроти static_cast. З того, що я прочитав, загальні правила - це використовувати статичний формат, коли типи можна інтерпретувати під час компіляції, отже, слово static. Це команда, яку компілятор C ++ використовує внутрішньо для неявних кастів.

reinterpret_casts застосовуються у двох сценаріях:

  • перетворити цілі типи в типи вказівників і навпаки
  • перетворити один тип вказівника в інший. Загальна думка, яку я розумію, це, що це малопомітно, і цього слід уникати.

Там, де я трохи заплутався - це одне використання, яке мені потрібно, я дзвоню C ++ з C і код C повинен триматися за об'єкт C ++, так що він в основному містить void*. Який склад слід використовувати для перетворення між void *типом та класом?

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


9
reinterpret_castне відбувається під час виконання. Вони обидва заяви про час збирання. З en.cppreference.com/w/cpp/language/reinterpret_cast : "На відміну від static_cast, але подібно до const_cast, вираз reinterpret_cast не компілюється в жодні інструкції процесора. Це суто директива компілятора, яка доручає компілятору обробляти послідовність бітів. (представлення об'єкта) вираження так, ніби він має тип new_type. "
Cris Luengo

@HeretoLearn, чи можна додати відповідні фрагменти коду з файлу * .c та * .cpp? Я думаю, що це може покращити виклад питання.
OrenIshShalom

Відповіді:


442

Стандарт 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слід віддати перевагу.


18
Мені подобається те, що 'b' не визначено. Це перешкоджає робити дурні речі. Якщо ви передаєте щось іншому типу вказівника, ви запитуєте про проблеми, а той факт, що ви не можете від цього залежати, робить вас більш обережними. Якщо ви використовували static_cast <> вище, яке значення 'b' все одно використовується?
Мартін Йорк

3
Я думав, що reinterpret_cast <> гарантує той самий біт. (що не відповідає дійсному вказівнику на інший тип).
Мартін Йорк

37
значення bC більше не визначено в C ++ 11 при використанні reinterpret_cast. І в C ++ 03 касти до int*to 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; }

7
Так, це стосується єдиного змістовного використання reinterpret_cast, про який я можу придумати.
jalf

8
Це може бути пізнім питанням, але чому постачальник API не використовує void*для цього?
Xeo

19
@Xeo Вони не використовують void *, оскільки тоді втрачають (деяку) перевірку типу під час компіляції.
jesup

4
Практичний випадок використання "непрозорих" типів даних - це коли ви хочете виставити API на C, але записати реалізацію в C ++. ICU - приклад бібліотеки, яка робить це в декількох місцях. Наприклад, в API перевірки підробляння ви маєте справу з покажчиками типу USpoofChecker*, де USpoofCheckerпорожня структура. Однак під капотом, коли ви проходите а USpoofChecker*, він піддається reinterpret_castвнутрішньому типу C ++.
sffc

@sffc чому б не виставити користувачеві тип Структури?
Гупта

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 відмовляється компілювати його (що правильно).


1
Гарний приклад! Я замінив би короткий на uint16_t і неподписаний char на uint8_t, щоб зробити його менш незрозумілим для людини.
Ян Туров

@ JanTuroň правда, ми не можемо припустити, що це short16 біт пам'яті. Виправлено.
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, які явно виключають повторне тлумачення_cast Тоді 7.1.5 Пункт 5 специфікатора constexpr (стор. 146) * Для не шаблонних функцій constexpr, що не знаходяться за умовчанням ... якщо немає значень аргументів, таких, що ... може бути оціненим підвиразком постійного вираження ядра (5.19 ), програма неправильно сформована *
Майкл Векслер

20

Значення reinterpret_castне визначається стандартом C ++. Отже, теоретично це reinterpret_castможе призвести до краху вашої програми. На практиці компілятори намагаються робити те, що ви очікуєте, - це інтерпретувати біти того, що ви передаєте, як би вони були типом, який ви кидаєте. Якщо ви знаєте, що компілятори, які ви збираєтеся використовувати, reinterpret_cast ви можете використовувати його, але сказати, що він є портативним, було б неправдою.

У випадку, який ви описуєте, і майже в будь-якому випадку, де ви можете розглянути reinterpret_cast, ви можете скористатись static_castякоюсь іншою альтернативою. Крім усього іншого, у стандарті є сказане про те, чого ви можете очікувати static_cast(§5.2.9):

Рівень типу "покажчик на cv void" може бути явно перетворений у вказівник на тип об'єкта. Значення вказівника типу для об’єкта, перетвореного в "покажчик на cv void" і назад до вихідного типу вказівника, матиме своє початкове значення.

Тож для вашого випадку використання, здається, досить зрозуміло, що комітет зі стандартизації призначений для вас static_cast.


5
Не зовсім збій вашої програми. Стандарт пропонує кілька гарантій щодо reinterpret_cast. Тільки не так багато, як часто очікують люди.
джелф

1
Не, якщо ви ним правильно користуєтеся. Тобто, reinterpret_cast від A до B до A є абсолютно безпечним і чітко визначеним. Але значення B не визначено, і так, якщо ви покладаєтесь на це, можуть трапитися погані речі. Але сам акторський склад є досить безпечним, доки ви використовували його тільки так, як це дозволяє стандарт. ;)
яльф

55
хаха, я підозрюю, що reinterpret_crash дійсно може збій вашої програми. Але переосмислити_cast не буде. ;)
jalf

5
<irony> Я спробував це на своєму компіляторі, і я якось відмовився від компіляції reinterpret_crash. Ні в якому разі помилка компілятора не зупинить мене на збої моєї програми реінтерпретації. Я повідомляю про помилку якнайшвидше! </
irony

18
@paercebaltemplate<class T, U> T reinterpret_crash(U a) { return *(T*)nullptr; }

12

Одне використання 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;                       // 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 Casts, але аналогічним 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
Orwellophile

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.
підкреслюй_

Що стосується c++2003стандарту я НЕ вважаю , що reinterpret_castробитьstatic_cast<cv T*>(static_cast<cv void*>(v))
Саша Zezulinsky

1
Гаразд, правда, але мене не хвилює версія від 13 років тому, і не слід також більшості кодерів, якщо (як це ймовірно) вони можуть її уникнути. Відповіді та зауваження дійсно повинні відображати останній доступний Стандарт, якщо інше не вказано ... IMHO. Як би там не було, я вважаю, що Комітет відчув необхідність чітко додати це після 2003 року (оскільки IIRC, це було те саме в C ++ 11)
підкреслюю

Перш ніж C++03це було C++98. Тони проектів використовували старий C ++ замість портативного C. Іноді доводиться дбати про переносимість. Наприклад, ви повинні підтримувати той самий код у Solaris, AIX, HPUX, Windows. Де мова йде про залежність компілятора та портативність, це складно. Тож хороший приклад введення reinterpret_cast
пекальної

знову ж таки, якщо, як я, ви раді обмежити себе лише платформами, які добре грають із останньою та найкращою версією мови, ваше заперечення - суперечка.
підкреслюй_

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

КОРОТКА: це означає, що однакова пам'ять використовується як інший тип. Таким чином, ви можете перетворити двійкові представлення плавців як тип int, як вище, в плавці. Наприклад, 0x80000000 дорівнює -0 (мантіса та показник є нульовими, але знак, msb, - це один. Це також працює для парних і довгих пар.

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

ПРИМІТКА. В обох випадках ви повинні зберегти закинуте значення у змінній перед виведенням! Цей макрос може допомогти:

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

Це правда, що "це означає, що однакова пам'ять використовується як інший тип", але вона обмежена певною парою типів. У вашому прикладі reinterpret_castформа intдо float&- невизначена поведінка.
jaskmar

1

Однією з причин використання reinterpret_castє те, коли базовий клас не має vtable, а похідний клас. У такому випадку, static_castі reinterpret_castце призведе до різних значень вказівника (це був би нетиповий випадок, згаданий вище за допомогою джальфа ). Як відмова від відповідальності, я не констатую, що це частина стандарту, а реалізація декількох розповсюджених компіляторів.

Як приклад, візьміть код нижче:

#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). Єдиним компілятором, який попередив про різницю, був кланг із:

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

Останнє застереження полягає в тому, що якщо базовий клас не має членів даних (наприклад, the 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 будується в пам'яті як дані, що стосуються В, спочатку за вбудованим A об'єктом. static_castПравильно повертає адресу об'єкта , прив'язаного A, і покажчик , створений static_castправильно дає значення поля даних. Покажчик, згенерований reinterpret_castлікуваннямb місцем розташування пам'яті, , що це звичайний об'єкт A, і коли вказівник намагається отримати поле даних, він повертає деякі дані, специфічні для В, як би вміст цього поля.

Одним із способів використання reinterpret_castє перетворення вказівника на непідписане ціле число (коли вказівники та цілі числа, які не підписуються, однакового розміру):

int i; unsigned int u = reinterpret_cast<unsigned int>(&i);


-6

Швидка відповідь: використовуйте, static_castякщо вона збирається, інакше вдайтеся до reinterpret_cast.


-16

Прочитайте FAQ ! Зберігання даних C ++ в C може бути ризикованим.

У C ++ вказівник на об'єкт може бути перетворений void *без жодних кастів. Але це неправда навпаки. Вам потрібно static_castбуде повернути оригінальний вказівник.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.