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

2497

Как правильно использовать:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast
  • С-стиль (type)value
  • Функциональный стиль type(value)

Как решить, что использовать в каких конкретных случаях?

e.James
источник
3
Для некоторых полезных конкретных примеров использования разного рода приведений вы можете проверить первый ответ на похожий вопрос в этой другой теме .
TeaMonkie
2
Вы можете найти действительно хорошие ответы на свой вопрос выше. Но я бы хотел добавить еще один момент, @ e.James: «Нет ничего, что эти новые операторы приведения типов в c ++ могут сделать, а в стиле c - нет. Они добавлены более или менее для лучшей читаемости кода».
BreakBadSP
@BreakBadSP Новые версии предназначены не только для лучшей читаемости кода. Они там, чтобы было сложнее делать опасные вещи, такие как отбрасывание констант или разыгрывание указателей вместо их значений. У static_cast гораздо меньше возможностей сделать что-то опасное, чем в стиле AC!
FourtyTwo
@FourtyTwo согласился
BreakBadSP

Ответы:

2572

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

static_castможет также приводиться через иерархии наследования. Это не нужно при приведении вверх (к базовому классу), но при приведении вниз его можно использовать до тех пор, пока оно не преобразуется через virtualнаследование. Однако он не выполняет проверку, и это неопределенное поведение - static_castприводить иерархию к типу, который на самом деле не является типом объекта.


const_castможет использоваться для удаления или добавления constк переменной; никакой другой C ++ cast не способен удалить его (даже не 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это самый опасный состав, и должен использоваться очень экономно. Он превращает один тип непосредственно в другой - например, приведение значения от одного указателя к другому или сохранение указателя в 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не удастся , Даже тогда рассмотрим более длинный и более явный вариант.

Приведения типа C также игнорируют управление доступом при выполнении a static_cast, что означает, что они имеют возможность выполнять операции, которые не могут выполнять другие приведения. Хотя это в основном клудж, и, на мой взгляд, это просто еще одна причина избегать бросков в стиле C.

коппро
источник
17
dynamic_cast предназначен только для полиморфных типов. вам нужно использовать его только при приведении к производному классу. static_cast, безусловно, является первым вариантом, если вам не нужна особенность dynamic_cast. Это не какой-то чудесный «бросок проверки типа» из серебряной пули вообще.
jalf
2
Отличный ответ! Одно быстрое замечание: static_cast может понадобиться для приведения иерархии в случае, если у вас есть Derived * & для приведения в Base * &, поскольку двойные указатели / ссылки не приводят к автоматическому приведению иерархии. Я сталкивался с такой (честно говоря, не совсем обычной) ситуацией две минуты назад. ;-)
bartgol
5
* "никакой другой C ++ cast не способен удалить const(даже не reinterpret_cast)" ... правда? Как насчет reinterpret_cast<int *>(reinterpret_cast<uintptr_t>(static_cast<int const *>(0)))?
user541686 15.01.15
29
Я думаю, что важная деталь, отсутствующая выше, заключается в том, что dynamic_cast имеет снижение производительности во время выполнения по сравнению со static или reinterpret_cast. Это важно, например, в программном обеспечении реального времени.
jfritz42
5
Возможно, стоит упомянуть, что reinterpret_castэто часто является предпочтительным оружием при работе с набором непрозрачных типов данных API
Class Skeleton
333

Используется dynamic_castдля преобразования указателей / ссылок в иерархии наследования.

Используйте static_castдля обычных преобразований типов.

Используйте reinterpret_castдля низкоуровневой реинтерпретации битовых комбинаций. Используйте с особой осторожностью.

Используйте const_castдля изгнания const/volatile. Избегайте этого, если только вы не застряли с использованием некорректного API-интерфейса.

Фред Ларсон
источник
2
Будьте осторожны с dynamic_cast. Он опирается на RTTI и не будет работать должным образом через границы общих библиотек. Просто потому, что вы создаете исполняемую и совместно используемую библиотеку независимо друг от друга, не существует стандартизированного способа синхронизации RTTI между различными сборками. По этой причине в библиотеке Qt существует qobject_cast <>, который использует информацию о типе QObject для проверки типов.
user3150128
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);
  .....
}

dynamic_cast:

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
Теория некоторых других ответов хороша, но все еще сбивает с толку, видя эти примеры после прочтения других ответов, действительно все они имеют смысл. То есть без примеров я все еще был не уверен, но с ними я теперь уверен в том, что означают другие ответы.
Solx
1
О последнем использовании reinterpret_cast: это не то же самое, что использование static_cast<char*>(&val)?
Лоренцо Белли
3
@LorenzoBelli Конечно нет. Вы пробовали это? Последний не является допустимым C ++ и блокирует компиляцию. static_castработает только между типами с определенными преобразованиями, видимым отношением по наследству или с / на void *. Для всего остального есть другие забросы. reinterpret castлюбому char *типу разрешено читать представление любого объекта - и это единственный из случаев, когда это ключевое слово полезно, а не безудержный генератор поведения реализации / неопределенности. Но это не считается «нормальным» преобразованием, поэтому не допускается (обычно) очень консервативным static_cast.
underscore_d
2
reinterpret_cast довольно распространен, когда вы работаете с системным программным обеспечением, таким как базы данных. В большинстве случаев вы пишете свой собственный менеджер страниц, который не знает, какой тип данных хранится на странице, и просто возвращает пустой указатель. Это до более высоких уровней, чтобы сделать переосмысление броска и сделать вывод, что они хотят.
Sohaib
1
Пример const_cast демонстрирует неопределенное поведение. Переменная, объявленная как const, не может быть отменена. Тем не менее, переменная, объявленная как неконстантная, которая передается функции, принимающей константную ссылку, может в этой функции быть деконстантной без UB.
Иоганн Герелл
99

Это может помочь, если вы знаете немного внутренностей ...

static_cast

  • Компилятор C ++ уже знает, как преобразовывать типы масштабирования, такие как float, в int. Используйте static_castдля них.
  • Когда вы просите компилятор преобразовать тип Aв B, static_castвызывается Bконструктор, передаваемый Aкак param. В качестве альтернативы, Aможет иметь оператор преобразования (т.е. A::operator B()). Если Bтакого конструктора Aнет или у вас нет оператора преобразования, вы получите ошибку времени компиляции.
  • Преобразование из A*в B*всегда выполняется успешно, если A и B находятся в иерархии наследования (или void), в противном случае вы получите ошибку компиляции.
  • Поправка : если вы приведете базовый указатель к производному указателю, но если фактический объект не является действительно производным типом, вы не получите ошибку. Вы получаете плохой указатель и, скорее всего, segfault во время выполнения. То же самое относится и A&к B&.
  • Попался : приведение от Derived к Base или наоборот создаст новую копию! Для людей, пришедших из C # / Java, это может быть огромным сюрпризом, поскольку в результате получается в основном отрубленный объект, созданный из Derived.

dynamic_cast

  • dynamic_cast использует информацию о типе среды выполнения, чтобы выяснить, является ли приведение действительным. Например, (Base*)чтобы (Derived*)может потерпеть неудачу , если указатель не на самом деле производного типа.
  • Это означает, что dynamic_cast очень дорогой по сравнению со static_cast!
  • Для A*чтобы B*, если бросок недействителен , то dynamic_cast вернет nullptr.
  • Для A&чтобы , B&если литой недействителен , то dynamic_cast выбросит bad_cast исключения.
  • В отличие от других приведений, есть накладные расходы времени выполнения.

const_cast

  • В то время как static_cast может делать не-const-const, он не может идти другим путем. Const_cast может работать в обоих направлениях.
  • Одним из примеров, где это удобно, является итерация по некоторому контейнеру, set<T>который возвращает свои элементы только как const, чтобы убедиться, что вы не измените его ключ. Однако, если вы хотите изменить неключевые члены объекта, тогда все должно быть в порядке. Вы можете использовать const_cast для удаления константности.
  • Другой пример, когда вы хотите реализовать, T& SomeClass::foo()а также const T& SomeClass::foo() const. Чтобы избежать дублирования кода, вы можете применить const_cast для возврата значения одной функции из другой.

reinterpret_cast

  • В основном это говорит о том, что возьмите эти байты в этой ячейке памяти и воспринимайте это как заданный объект.
  • Например, вы можете загрузить 4 байта с плавающей точкой до 4 байтов с целым числом, чтобы увидеть, как выглядят биты в плавающей точке.
  • Очевидно, что если данные не соответствуют типу, вы можете получить 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, который может привести к segfault во время выполнения, если вам повезет. 2. Динамические броски могут также использоваться в перекрестном литье. 3. Const броски могут привести к UB в некоторых случаях. Использование mutableможет быть лучшим выбором для реализации логической константности.
Адриан
1
@ Адриан, ты прав во всех отношениях. Ответ написан для людей с более или менее начальным уровнем, и я не хотел перегружать их всеми остальными осложнениями mutable, кросс-кастингом и т. Д.
Shital Shah
16

Имеет ли это ответ на ваш вопрос?

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

Андреас Buykx
источник
3
Я использовал reintrepret_cast для одной цели - получить биты из двойного (такого же размера, как long на моей платформе).
Иисус Навин
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по одной причине. Я видел необработанные данные объекта, хранящиеся в типе данных «blob» в базе данных, а затем, когда данные извлекаются из базы данных, 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 заключается в том, что они скрывают реальные намерения разработчиков. Приведения в стиле C могут выполнять практически все типы приведения: от обычно безопасных приведения, выполняемых static_cast <> () и dynamic_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-стилями.

Вот небольшая цитата из книги Бьярна Страуструпа (автора C ++) «Язык программирования C ++, 4-е издание» - стр. 302.

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

Timmy_A
источник
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 может использоваться для преобразования указателя на объект в указатель на любой не связанный тип объекта.

Следует отметить следующее: dynamic_cast не будет работать во время выполнения, однако на большинстве компиляторов он также не будет компилироваться, поскольку в структуре преобразуемого указателя нет виртуальных функций, то есть dynamic_cast будет работать только с полиморфными указателями классов. ,

Когда использовать C ++ cast :

  • Используйте static_cast в качестве эквивалента приведения типа C, который выполняет преобразование значений, или когда нам нужно явно преобразовать указатель из класса в его суперкласс.
  • Используйте const_cast, чтобы удалить квалификатор const.
  • Используйте reinterpret_cast для небезопасных преобразований типов указателей в целочисленные и из других типов указателей и обратно. Используйте это, только если мы знаем, что делаем, и понимаем проблемы псевдонимов.
Панкадж Кумар Тхапа
источник
3

static_castпо dynamic_castсравнению с reinterpret_castвнутренним видом на downcast / upcast

В этом ответе я хочу сравнить эти три механизма на конкретном примере upcast / downcast и проанализировать, что происходит с базовыми указателями / памятью / сборкой, чтобы дать конкретное понимание того, как они сравниваются.

Я считаю, что это даст хорошую интуицию о том, как эти броски отличаются:

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

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

    Эта проверка безопасности позволяет вам запрашивать, имеет ли указатель базового класса заданный тип во время выполнения, проверяя возвращение nullptrкоторого указывает на недопустимое снижение.

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

    Если прерывание - это единственное действие, которое может выполнить ваш код, возможно, вам нужно только включить dynamic_castin in debug builds ( -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внутренне.

Поэтому мы приходим к критическому выводу:

upcast или downcast нужно только сместить значение указателя на значение, известное во время компиляции

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

b2s[1] = &d;

за исключением того, что у этого есть vtable 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, если значение einput равно NULL.

    В противном случае он устанавливает некоторые аргументы в RDX, RSI и RDI и вызовах __dynamic_cast.

    У меня нет терпения, чтобы проанализировать это сейчас, но, как говорили другие, единственный способ для этого - __dynamic_castполучить доступ к некоторым дополнительным структурам данных RTTI в памяти, которые представляют иерархию классов.

    Поэтому он должен начинаться с B2записи для этой таблицы, а затем обходить эту иерархию классов, пока не обнаружит, что vtable для типа Dприведен из b2s[0].

    Вот почему реинтерпретация броска потенциально дорогая! Вот пример, где один патч, конвертирующий dynamic_casta static_castв сложный проект, сократил время выполнения на 33%! ,

  • reinterpret_cast<D*>(b2s[1]) 0x7fffffffc940этот просто верит нам слепо: мы сказали, что есть Dадрес at b2s[1], а компилятор не выполняет вычисления смещения.

    Но это неправильно, потому что D на самом деле в 0x7fffffffc930, то, что в 0x7fffffffc940 - это B2-подобная структура внутри 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.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
источник