Коли слід використовувати static_cast, dynam_cast, const_cast та reinterpret_cast?


2490

Які належні способи використання:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • Акторський склад у стилі C (type)value
  • Акторський склад type(value)

Як можна вирішити, що використовувати в яких конкретних випадках?



3
Для деяких корисних конкретних прикладів використання роликів різного виду ви можете перевірити першу відповідь на подібне запитання в цій іншій темі .
TeaMonkie

2
Ви можете знайти справді хороші відповіді на своє запитання вище. Але я хотів би поставити тут ще одну точку, @ e.James "Немає нічого, що можуть робити ці нові оператори кидків c ++, а передати стиль c не можна. Вони додаються більш-менш для кращої читабельності коду".
BreakBadSP

@BreakBadSP Нові касти не лише для кращої читабельності коду. Вони там, щоб зробити це складніше робити небезпечні речі, як, наприклад, відкидати const або відкидати покажчики замість своїх значень. static_cast має набагато менше можливостей зробити щось небезпечне, ніж акторський стиль!
FourtyTwo

@FourtyTwo погодився
BreakBadSP

Відповіді:


2569

static_castце перший склад, який ви повинні спробувати використати. Він робить такі речі, як неявні перетворення між типами (наприклад, intдо floatабо вказівник на void*), а також може викликати явні функції перетворення (або неявні). У багатьох випадках прямо заявляти static_castне потрібно, але важливо зазначити, що T(something)синтаксис еквівалентний (T)somethingі його слід уникати (докладніше про це пізніше). Однак T(something, something_else)це безпечно і гарантовано дзвонить конструктору.

static_castможе також містити ієрархії спадкування. Це є непотрібним при закиданні вгору (до базового класу), але при закиданні вниз його можна використовувати до тих пір, поки це не буде передано через virtualспадщину. Однак це не робить перевірки, і це не визначене поведінка, щоб static_castзнизити ієрархію до типу, який насправді не є типом об'єкта.


const_castможе використовуватися для видалення або додавання constзмінної; жоден інший склад C ++ не може його видалити (навіть не reinterpret_cast). Важливо зауважити, що зміна колишнього constзначення не визначається лише тоді, коли початкова змінна є const; якщо ви використовуєте його, щоб зняти constпосилання на те, що не було оголошено const, це безпечно. Це може бути корисно, наприклад, при перевантаженні функцій членів на основі const. Він також може використовуватися для додавання constдо об'єкта, наприклад, для виклику перевантаження функції члена.

const_castтакож працює аналогічно volatile, хоча це рідше.


dynamic_castвикористовується виключно для боротьби з поліморфізмом. Ви можете передавати покажчик або посилання на будь-який поліморфний тип до будь-якого іншого типу класу (поліморфний тип має принаймні одну віртуальну функцію, оголошену або успадковану). Ви можете використовувати його для більш ніж просто закидання вниз - ви можете кинути набік або навіть підняти інший ланцюжок. dynamic_castБуде шукати потрібний об'єкт і повернути його , якщо це можливо. Якщо він не зможе, він повернеться nullptrу разі вказівника або викине std::bad_castу випадку посилання.

dynamic_castОднак є деякі обмеження. Це не працює, якщо в ієрархії спадкування є декілька об'єктів одного типу (так званий "страшний діамант") і ви не використовуєте virtualспадкування. Вона також може йти тільки через публічне спадкування - це завжди буде не в подорож через protectedабо privateуспадкування. Однак це рідко є проблемою, оскільки такі форми успадкування є рідкісними.


reinterpret_castє найнебезпечнішим складом, і його слід використовувати дуже щадно. Він перетворює один тип безпосередньо в інший - наприклад, передачу значення з одного вказівника на інший, або зберігання вказівника у an intабо всілякі інші неприємні речі. В основному, єдиною гарантією, яку ви отримуєте, reinterpret_castє те, що зазвичай, якщо ви повернете результат до початкового типу, ви отримаєте точно таке ж значення (але не, якщо проміжний тип менший, ніж вихідний тип). Існує ряд конверсій, які reinterpret_castтеж не можуть зробити. Він використовується в основному для особливо дивних перетворень і маніпуляцій бітом, як перетворення необробленого потоку даних у фактичні дані або зберігання даних у низьких бітах вказівника до вирівняних даних.


C-стиль лиття і функція стиль кидок є зліпки з використанням (type)objectабо type(object), відповідно, і функціонально еквівалентні. Вони визначаються як перший із наступних, що вдається:

  • const_cast
  • static_cast (хоча ігноруючи обмеження доступу)
  • static_cast (див. вище), потім const_cast
  • reinterpret_cast
  • reinterpret_cast, тоді const_cast

Тому в деяких випадках він може використовуватися як заміна для інших ролей, але може бути вкрай небезпечним через здатність переходити в a reinterpret_cast, і останнім слід віддати перевагу тоді, коли потрібне явне кастинг, якщо ви не впевнені static_cast, що вдасться чи reinterpret_castне вдасться . Вже тоді розглянемо більш тривалий, чіткіший варіант.

Заготівки в стилі С також ігнорують контроль доступу під час виконання значка "" static_cast, що означає, що вони мають можливість виконувати операцію, яку не може виконати жодна інша особа. Це, головним чином, хитрість, і, на мою думку, це лише ще одна причина, щоб уникнути кастингу в стилі С.


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

2
Чудова відповідь! Одне швидке зауваження: static_cast може знадобитися для того, щоб створити ієрархію, якщо у вас є Похідне * & для передачі в Base * &, оскільки подвійні вказівники / посилання не автоматично піддають ієрархії. Я натрапив на таку (чесно кажучи, не звичайну) ситуацію дві хвилини тому. ;-)
бартголь

5
* "жоден інший склад C ++ не здатний видалити const(навіть не reinterpret_cast)" ... дійсно? Про що reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
користувач541686

29
Я думаю, що важливою деталлю, пропущеною вище, є те, що в динамичному випуску покарання виконується за час виконання часу порівняно зі статичним або реінтерпретаційним. Це важливо, наприклад, у програмному забезпеченні в реальному часі.
jfritz42

5
Можливо, варто згадати, що reinterpret_castчасто це зброя вибору при роботі з набором непрозорих типів даних API
Клас Скелет

333

Використовувати dynamic_castдля перетворення покажчиків / посилань у межах ієрархії спадкування.

Використовуйте static_castдля перетворень звичайного типу.

Використовуйте reinterpret_castдля низькорівневої переінтерпретації бітових шаблонів. Використовуйте з особливою обережністю.

Використовуйте const_castдля відкидання const/volatile. Уникайте цього, якщо ви не застрягли, використовуючи некоректно API.


2
Будьте обережні з динамічним_кастом. Він покладається на RTTI, і це не буде працювати, як очікувалося, за межами спільних бібліотек. Просто тому, що ви самостійно створюєте виконувану та спільну бібліотеку, не існує стандартизованого способу синхронізації RTTI у різних збірках. З цієї причини в бібліотеці Qt існує qobject_cast <>, який використовує інформацію про тип QObject для перевірки типів.
користувач3150128

198

(Вище було дано багато теоретичних та концептуальних пояснень)

Нижче наведені деякі з практичних прикладів , коли я використовував static_cast , dynamic_cast , const_cast , reinterpret_cast .

(Звертається також до цього, щоб зрозуміти пояснення: http://www.cplusplus.com/doc/tutorial/typecasting/ )

static_cast:

OnEventData(void* pData)

{
  ......

  //  pData is a void* pData, 

  //  EventData is a structure e.g. 
  //  typedef struct _EventData {
  //  std::string id;
  //  std:: string remote_id;
  //  } EventData;

  // On Some Situation a void pointer *pData
  // has been static_casted as 
  // EventData* pointer 

  EventData *evtdata = static_cast<EventData*>(pData);
  .....
}

динамічний_каст:

void DebugLog::OnMessage(Message *msg)
{
    static DebugMsgData *debug;
    static XYZMsgData *xyz;

    if(debug = dynamic_cast<DebugMsgData*>(msg->pdata)){
        // debug message
    }
    else if(xyz = dynamic_cast<XYZMsgData*>(msg->pdata)){
        // xyz message
    }
    else/* if( ... )*/{
        // ...
    }
}

const_cast:

// *Passwd declared as a const

const unsigned char *Passwd


// on some situation it require to remove its constness

const_cast<unsigned char*>(Passwd)

reinterpret_cast:

typedef unsigned short uint16;

// Read Bytes returns that 2 bytes got read. 

bool ByteBuffer::ReadUInt16(uint16& val) {
  return ReadBytes(reinterpret_cast<char*>(&val), 2);
}

31
Теорія деяких інших відповідей хороша, але все ж заплутана, якщо побачити ці приклади після прочитання інших відповідей, насправді має сенс усі вони. Тобто без прикладів я все ще був не впевнений, але з ними я зараз впевнений у тому, що означають інші відповіді.
Солкс

1
Про останнє використання reinterpret_cast: це не те саме, що використання static_cast<char*>(&val)?
Лоренцо Беллі

3
@LorenzoBelli Звичайно, ні. Ви пробували? Останній недійсний на C ++ і блокує компіляцію. static_castпрацює лише між типами з визначеними перетвореннями, видимим відношенням за спадщиною або з / з void *. Для всього іншого є інші касти. reinterpret castбудь-якому char *типу дозволено дозволяти читати представлення будь-якого об’єкта - і один з єдиних випадків, коли це ключове слово корисне, а не розгалужений генератор поведінки втілення / невизначеності. Але це не вважається "нормальним" перетворенням, тому не допускається (як правило) дуже консервативно static_cast.
підкреслюй_d

2
reinterpret_cast є досить поширеним, коли ви працюєте із системним програмним забезпеченням, таким як бази даних. У більшості випадків ви пишете власний менеджер сторінок, який не має уявлення про тип даних, що зберігається на сторінці, і просто повертає недійсний покажчик. Це до вищих рівнів, щоб зробити переосмислений акторський склад і зробити висновок про нього як завгодно.
Sohaib

1
Приклад const_cast демонструє не визначене поведінку. Змінна, оголошена як const, не може бути де-const-ed. Однак змінна, оголошена як non-const, яка передається функції, що приймає посилання на const, може бути усунена в цій функції, не будучи UB.
Йоганн Герелл

99

Це може допомогти, якщо ви знаєте трохи внутрішніх справ ...

static_cast

  • Компілятор C ++ вже знає, як конвертувати між типом масштабування, таким як float в int. Використовуйте static_castдля них.
  • Коли ви попросите компілятор перетворити з типу Aв B, конструктор static_castвикликів Bпередає Aяк парам. Крім того, Aможе мати оператор перетворення (тобто A::operator B()). Якщо Bтакого конструктора Aнемає або немає оператора перетворення, ви отримуєте помилку часу компіляції.
  • Cast від A*до B*завжди успішна , якщо А і В знаходяться в ієрархії успадкування (або порожнечі) в іншому випадку ви отримаєте помилку компіляції.
  • Gotcha : Якщо ви кидаєте базовий покажчик на похідний вказівник, але якщо фактичний об'єкт не є дійсно похідним типом, ви не помилитесь. Ви отримуєте поганий покажчик і, швидше за все, segfault під час виконання. Те ж саме відноситься і A&до B&.
  • Gotcha : Cast from Derived to Base або viceversa створює нову копію! Для людей, які приїжджають із C # / Java, це може бути величезним сюрпризом, оскільки результат - це в основному відрізаний об’єкт, створений від Derived.

динамічна передача

  • динамичний_кас використовує інформацію про тип виконання, щоб визначити, чи є передача дійсною. Наприклад, (Base*)вийти з (Derived*)ладу, якщо покажчик насправді не має похідного типу.
  • Це означає, що динамічний_cast дуже дорогий порівняно зі статичним_кастом!
  • Якщо A*для B*, якщо cast недійсний, тоді динамичний_cast поверне nullptr.
  • Для того, A&що B&якщо команда cast недійсна, динамічний_cast закидає виняток bad_cast.
  • На відміну від інших ролей, час виконання триває.

const_cast

  • У той час як static_cast може робити non-const, щоб const, він не може йти іншим способом. Здійснювати const_cast можна обома способами.
  • Одним із прикладів, коли це стане в нагоді, є перегляд через якийсь контейнер, set<T>який повертає його елементи лише як const, щоб переконатися, що ви не змінюєте його ключ. Однак якщо ваш намір полягає в зміні не-ключових членів об'єкта, тоді це повинно бути нормально. Ви можете використовувати const_cast для видалення constness.
  • Інший приклад, коли ви хочете реалізувати T& SomeClass::foo(), а також const T& SomeClass::foo() const. Щоб уникнути дублювання коду, ви можете застосувати const_cast, щоб повернути значення однієї функції з іншої.

reinterpret_cast

  • В основному це говорить про те, що візьміть ці байти в цьому місці пам'яті і вважайте це даним об'єктом.
  • Наприклад, ви можете завантажити 4 байти float до 4 байт int, щоб побачити, як виглядають біти в float.
  • Очевидно, якщо дані невірні для типу, ви можете отримати segfault.
  • Немає накладних витрат для цього акторського складу.

Я додав інформацію про оператора перетворення, але є кілька інших речей, які також слід виправити, і мені не здається, що це надто зручне оновлення. Елементи: 1. If you cast base pointer to derived pointer but if actual object is not really derived type then you don't get error. You get bad pointer and segfault at runtime.Ви отримуєте UB, що може призвести до сегментації під час виконання, якщо вам пощастить. 2. Динамічні лиття також можуть використовуватися при перехресному литті. 3. Конст-фусти в деяких випадках можуть спричинити за собою UB. Використання mutableможе бути кращим вибором для реалізації логічної стійкості.
Адріан

1
@ Адріан, ти в усьому правильний. Відповідь написана для людей на більш-менш початкових рівнях, і я не хотів переповнювати їх усіма іншими ускладненнями, які виникають mutable, перехресне кастинг тощо.
Shital Shah

16

Чи відповідає це на ваше запитання?

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


3
Я використовував reintrepret_cast для однієї мети - отримання бітів з подвійного (того ж розміру, як довго на моїй платформі).
Джошуа

2
reinterpret_cast потрібен, наприклад, для роботи з COM-об'єктами. CoCreateInstance () має вихідний параметр типу void ** (останній параметр), в якому ви передасте свій покажчик, оголошений, наприклад, "INetFwPolicy2 * pNetFwPolicy2". Для цього вам потрібно написати щось на кшталт reinterpret_cast <void **> (& pNetFwPolicy2).
Серж Рогач

1
Можливо, є інший підхід, але я використовую reinterpret_castдля витягування фрагментів даних з масиву. Наприклад, якщо у мене є char*великий буфер, повний упакованих бінарних даних, які мені потрібно перенести і отримати окремі примітиви різних типів. Приблизно так:template<class ValType> unsigned int readValFromAddress(char* addr, ValType& val) { /*On platforms other than x86(_64) this could do unaligned reads, which could be bad*/ val = (*(reinterpret_cast<ValType*>(addr))); return sizeof(ValType); }
Джеймс Матта

Я ніколи не використовував reinterpret_cast, для цього не дуже багато застосувань.
Піка Чарівник китів

Особисто я коли-небудь бачив reinterpret_castвикористаний з однієї причини. Я бачив необроблені дані об'єкта, що зберігаються в "базі" типу даних у базі даних, тоді, коли дані отримані з бази даних, reinterpret_castвикористовуються для перетворення цих необроблених даних у об'єкт.
ImaginaryHuman072889

15

Окрім інших поки що відповідей, ось очевидний приклад, коли static_castцього недостатньо, щоб це reinterpret_castбуло потрібно. Припустимо, є функція, яка у вихідному параметрі повертає покажчики на об’єкти різних класів (які не мають загального базового класу). Справжнім прикладом такої функції є CoCreateInstance()(див. Останній параметр, який є насправді void**). Припустимо, ви вимагаєте від цієї функції певного класу об'єктів, тому ви заздалегідь знаєте тип для вказівника (що ви часто робите для об’єктів COM). У цьому випадку ви не можете кинути вказівник на ваш покажчик за void**допомогою static_cast: вам потрібно reinterpret_cast<void**>(&yourPointer).

У коді:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    //static_cast<void**>(&pNetFwPolicy2) would give a compile error
    reinterpret_cast<void**>(&pNetFwPolicy2) );

Однак static_castпрацює для простих покажчиків (не покажчиків на покажчики), тому вищевказаний код можна переписати, щоб уникнути reinterpret_cast(за ціною додаткової змінної) наступним чином:

#include <windows.h>
#include <netfw.h>
.....
INetFwPolicy2* pNetFwPolicy2 = nullptr;
void* tmp = nullptr;
HRESULT hr = CoCreateInstance(__uuidof(NetFwPolicy2), nullptr,
    CLSCTX_INPROC_SERVER, __uuidof(INetFwPolicy2),
    &tmp );
pNetFwPolicy2 = static_cast<INetFwPolicy2*>(tmp);

Не вдалося б це зробити щось на зразок &static_cast<void*>(pNetFwPolicy2)замість static_cast<void**>(&pNetFwPolicy2)?
jp48

9

Хоча в інших відповідях добре описані всі відмінності між C ++ кастрами, я хотів би додати коротке зауваження, чому ви не повинні використовувати касти в стилі C (Type) varі Type(var).

Для початківців C ++ касти в стилі C виглядають як операція суперсети над C ++ кастрами (static_cast <> (), dynamic_cast <> (), const_cast <> (), reinterpret_cast <> ()), і хтось міг би віддавати перевагу їм над кастрами C ++ . Насправді акторський склад у стилі С - це суперсеть і коротше писати.

Основна проблема кастингу в стилі C полягає в тому, що вони приховують в розробника реальні наміри акторів. Заготівки в стилі C можуть виконувати практично всі типи кастингу від звичайно безпечних ролях, виконаних static_cast <> () та dinamiні_cast <> (), до потенційно небезпечних ролей, таких як const_cast <> (), де модифікатор const можна видалити, щоб змінні const може бути змінено та reinterpret_cast <> (), що навіть може повторно інтерпретувати цілі значення в покажчики.

Ось зразок.

int a=rand(); // Random number.

int* pa1=reinterpret_cast<int*>(a); // OK. Here developer clearly expressed he wanted to do this potentially dangerous operation.

int* pa2=static_cast<int*>(a); // Compiler error.
int* pa3=dynamic_cast<int*>(a); // Compiler error.

int* pa4=(int*) a; // OK. C-style cast can do such cast. The question is if it was intentional or developer just did some typo.

*pa4=5; // Program crashes.

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

Ось коротка цитата з книги Bjarne Stroustrup (автора програми C ++) 4-е видання Мова програмування C ++ - стор. 302.

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


5

Щоб зрозуміти, розглянемо нижче фрагмент коду:

struct Foo{};
struct Bar{};

int main(int argc, char** argv)
{
    Foo* f = new Foo;

    Bar* b1 = f;                              // (1)
    Bar* b2 = static_cast<Bar*>(f);           // (2)
    Bar* b3 = dynamic_cast<Bar*>(f);          // (3)
    Bar* b4 = reinterpret_cast<Bar*>(f);      // (4)
    Bar* b5 = const_cast<Bar*>(f);            // (5)

    return 0;
}

Тільки рядок (4) збирається без помилок. Лише reinterpret_cast може використовуватися для перетворення вказівника на об'єкт до вказівника до будь-якого непов'язаного типу об'єкта.

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

Коли використовувати C ++ ролик :

  • Використовуйте static_cast в якості еквіваленту анотації в стилі C, яка робить перетворення значень, або коли нам потрібно явно передати покажчик з класу на його суперклас.
  • Використовуйте const_cast для видалення класифікатора const.
  • Використовуйте reinterpret_cast, щоб зробити небезпечні перетворення типів вказівників на цілі чи інші типи вказівників. Використовуйте це лише в тому випадку, якщо ми знаємо, що робимо, і розуміємо проблеми, що виникають.

2

static_castvs dynamic_castпроти reinterpret_castвнутрішніх поглядів на низхідний / понівечений

У цій відповіді я хочу порівняти ці три механізми на конкретному прикладі оновлення / знищення та проаналізувати, що відбувається з основними вказівниками / пам'яттю / складанням, щоб дати конкретне розуміння того, як вони порівнюються.

Я вважаю, що це дасть добру інтуїцію щодо того, чим відрізняються ці ролі:

  • static_cast: чи зміщується одна адреса під час виконання (низький вплив часу виконання) і чи не перевіряє безпеку правильність спаду.

  • dyanamic_cast: чи однакове зміщення адреси під час виконання, як і static_cast, але також і дорога безпека перевіряє правильність зниження інформації за допомогою RTTI.

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

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

    Якщо переривання - це єдине дію, яке може здійснити ваш код, можливо, ви хочете лише ввімкнути dynamic_castвбудовані помилки ( -NDEBUG) і використовувати static_castінше, наприклад, як це зроблено тут , щоб не уповільнити швидкий пробіг.

  • reinterpret_cast: не робить нічого під час виконання, навіть зміщення адреси. Вказівник повинен точно вказувати на правильний тип, навіть базовий клас не працює. Ви, як правило, цього не бажаєте, якщо не будуть залучені необмежені потоки байтів.

Розглянемо наступний приклад коду:

main.cpp

#include <iostream>

struct B1 {
    B1(int int_in_b1) : int_in_b1(int_in_b1) {}
    virtual ~B1() {}
    void f0() {}
    virtual int f1() { return 1; }
    int int_in_b1;
};

struct B2 {
    B2(int int_in_b2) : int_in_b2(int_in_b2) {}
    virtual ~B2() {}
    virtual int f2() { return 2; }
    int int_in_b2;
};

struct D : public B1, public B2 {
    D(int int_in_b1, int int_in_b2, int int_in_d)
        : B1(int_in_b1), B2(int_in_b2), int_in_d(int_in_d) {}
    void d() {}
    int f2() { return 3; }
    int int_in_d;
};

int main() {
    B2 *b2s[2];
    B2 b2{11};
    D *dp;
    D d{1, 2, 3};

    // The memory layout must support the virtual method call use case.
    b2s[0] = &b2;
    // An upcast is an implicit static_cast<>().
    b2s[1] = &d;
    std::cout << "&d           " << &d           << std::endl;
    std::cout << "b2s[0]       " << b2s[0]       << std::endl;
    std::cout << "b2s[1]       " << b2s[1]       << std::endl;
    std::cout << "b2s[0]->f2() " << b2s[0]->f2() << std::endl;
    std::cout << "b2s[1]->f2() " << b2s[1]->f2() << std::endl;

    // Now for some downcasts.

    // Cannot be done implicitly
    // error: invalid conversion from ‘B2*’ to ‘D*’ [-fpermissive]
    // dp = (b2s[0]);

    // Undefined behaviour to an unrelated memory address because this is a B2, not D.
    dp = static_cast<D*>(b2s[0]);
    std::cout << "static_cast<D*>(b2s[0])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[0])->int_in_d  " << dp->int_in_d << std::endl;

    // OK
    dp = static_cast<D*>(b2s[1]);
    std::cout << "static_cast<D*>(b2s[1])            " << dp           << std::endl;
    std::cout << "static_cast<D*>(b2s[1])->int_in_d  " << dp->int_in_d << std::endl;

    // Segfault because dp is nullptr.
    dp = dynamic_cast<D*>(b2s[0]);
    std::cout << "dynamic_cast<D*>(b2s[0])           " << dp           << std::endl;
    //std::cout << "dynamic_cast<D*>(b2s[0])->int_in_d " << dp->int_in_d << std::endl;

    // OK
    dp = dynamic_cast<D*>(b2s[1]);
    std::cout << "dynamic_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "dynamic_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;

    // Undefined behaviour to an unrelated memory address because this
    // did not calculate the offset to get from B2* to D*.
    dp = reinterpret_cast<D*>(b2s[1]);
    std::cout << "reinterpret_cast<D*>(b2s[1])           " << dp           << std::endl;
    std::cout << "reinterpret_cast<D*>(b2s[1])->int_in_d " << dp->int_in_d << std::endl;
}

Компілюйте, запустіть і розберіть:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
setarch `uname -m` -R ./main.out
gdb -batch -ex "disassemble/rs main" main.out

де setarchце використовується , щоб відключити ASLR , щоб полегшити порівняння прогонів.

Можливий вихід:

&d           0x7fffffffc930
b2s[0]       0x7fffffffc920
b2s[1]       0x7fffffffc940
b2s[0]->f2() 2
b2s[1]->f2() 3
static_cast<D*>(b2s[0])            0x7fffffffc910
static_cast<D*>(b2s[0])->int_in_d  1
static_cast<D*>(b2s[1])            0x7fffffffc930
static_cast<D*>(b2s[1])->int_in_d  3
dynamic_cast<D*>(b2s[0])           0
dynamic_cast<D*>(b2s[1])           0x7fffffffc930
dynamic_cast<D*>(b2s[1])->int_in_d 3
reinterpret_cast<D*>(b2s[1])           0x7fffffffc940
reinterpret_cast<D*>(b2s[1])->int_in_d 32767

Тепер, як згадувалося на веб- сайті: https://en.wikipedia.org/wiki/Virtual_method_table , щоб ефективно підтримувати виклики віртуального методу, структура даних пам'яті Dмає виглядати приблизно так:

B1:
  +0: pointer to virtual method table of B1
  +4: value of int_in_b1

B2:
  +0: pointer to virtual method table of B2
  +4: value of int_in_b2

D:
  +0: pointer to virtual method table of D (for B1)
  +4: value of int_in_b1
  +8: pointer to virtual method table of D (for B2)
 +12: value of int_in_b2
 +16: value of int_in_d

Ключ в тому, що структура даних пам'яті Dмістить всередині нього структуру пам'яті сумісна з B1і з B2внутрішньо.

Тому ми дійшли критичного висновку:

оновленому або похилому стану потрібно лише змістити значення вказівника на значення, відоме під час компіляції

Таким чином, коли Dпередається до масиву базового типу, тип передачі фактично обчислює це зміщення і вказує щось, що виглядає точно як дійсне B2в пам'яті:

b2s[1] = &d;

за винятком того, що один має для віртуальні таблиці Dзамість B2, і тому все віртуальні виклики працюють прозоро.

Тепер ми можемо нарешті повернутися до типу кастингу та аналізу нашого конкретного прикладу.

З виводу stdout ми бачимо:

&d           0x7fffffffc930
b2s[1]       0x7fffffffc940

Отже, static_castзроблені там неявні Dпомилки правильно обчислили зміщення від повної структури даних на 0x7fffffffc930 до B2подібної, яка знаходиться на 0x7fffffffc940. Ми також робимо висновок, що те, що лежить між 0x7fffffffc930 та 0x7fffffffc940, ймовірно, є B1даними та vtable.

Тоді в розділах, що перебувають у спадному стані, тепер легко зрозуміти, як недійсні невдачі і чому:

  • static_cast<D*>(b2s[0]) 0x7fffffffc910: компілятор просто збільшився на 0x10 під час компіляції, щоб спробувати перейти від a B2до вмістуD

    Але оскільки b2s[0]це не було D, це вказує на невизначений регіон пам'яті.

    Розбирання:

    49          dp = static_cast<D*>(b2s[0]);
       0x0000000000000fc8 <+414>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fcc <+418>:   48 85 c0        test   %rax,%rax
       0x0000000000000fcf <+421>:   74 0a   je     0xfdb <main()+433>
       0x0000000000000fd1 <+423>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x0000000000000fd5 <+427>:   48 83 e8 10     sub    $0x10,%rax
       0x0000000000000fd9 <+431>:   eb 05   jmp    0xfe0 <main()+438>
       0x0000000000000fdb <+433>:   b8 00 00 00 00  mov    $0x0,%eax
       0x0000000000000fe0 <+438>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    тому ми бачимо, що GCC робить:

    • перевірте, чи вказівник NULL, а якщо так, поверніть NULL
    • інакше відніміть від нього 0x10, щоб досягти того, Dякого не існує
  • dynamic_cast<D*>(b2s[0]) 0: C ++ фактично виявив, що акторський склад недійсний і повернувся nullptr!

    Це неможливо зробити під час компіляції, і ми підтвердимо це з розбирання:

    59          dp = dynamic_cast<D*>(b2s[0]);
       0x00000000000010ec <+706>:   48 8b 45 d0     mov    -0x30(%rbp),%rax
       0x00000000000010f0 <+710>:   48 85 c0        test   %rax,%rax
       0x00000000000010f3 <+713>:   74 1d   je     0x1112 <main()+744>
       0x00000000000010f5 <+715>:   b9 10 00 00 00  mov    $0x10,%ecx
       0x00000000000010fa <+720>:   48 8d 15 f7 0b 20 00    lea    0x200bf7(%rip),%rdx        # 0x201cf8 <_ZTI1D>
       0x0000000000001101 <+727>:   48 8d 35 28 0c 20 00    lea    0x200c28(%rip),%rsi        # 0x201d30 <_ZTI2B2>
       0x0000000000001108 <+734>:   48 89 c7        mov    %rax,%rdi
       0x000000000000110b <+737>:   e8 c0 fb ff ff  callq  0xcd0 <__dynamic_cast@plt>
       0x0000000000001110 <+742>:   eb 05   jmp    0x1117 <main()+749>
       0x0000000000001112 <+744>:   b8 00 00 00 00  mov    $0x0,%eax
       0x0000000000001117 <+749>:   48 89 45 98     mov    %rax,-0x68(%rbp)

    Спочатку відбувається перевірка NULL, і вона повертає NULL, якщо e-вхід NULL.

    В іншому випадку він встановлює деякі аргументи в RDX, RSI та RDI та викликах __dynamic_cast.

    Зараз у мене немає терпіння аналізувати це далі, але, як говорили інші, єдиний спосіб для цього - це __dynamic_castотримати доступ до деяких додаткових структур даних пам'яті RTTI, які представляють ієрархію класів.

    Тому він повинен починати з B2запису для цієї таблиці, а потім ходити по цій ієрархії класів, поки не виявить, що vtable для Dtypecast від b2s[0].

    Ось чому переосмислена ролях потенційно дорога! Ось приклад, коли патч одного вкладиша, який перетворює dynamic_casta static_castв складний проект, скоротив час виконання на 33%! .

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940цей нам просто сліпо вірить: ми сказали, що є Dадреса b2s[1], і компілятор не робить компенсації зсуву.

    Але це неправильно, тому що D насправді знаходиться на 0x7fffffffc930, що в 0x7fffffffc940 - це структура, подібна до В2 всередині D! Тож доступ до сміття отримує доступ.

    Ми можемо підтвердити це з жахливої -O0збірки, яка просто переміщує значення навколо:

    70          dp = reinterpret_cast<D*>(b2s[1]);
       0x00000000000011fa <+976>:   48 8b 45 d8     mov    -0x28(%rbp),%rax
       0x00000000000011fe <+980>:   48 89 45 98     mov    %rax,-0x68(%rbp)

Пов’язані запитання:

Тестовано на Ubuntu 18.04 amd64, GCC 7.4.0.

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