приведение через void * вместо использования reinterpret_cast - PullRequest
37 голосов
/ 08 декабря 2009

Я читаю книгу, и обнаружил, что reinterpret_cast не следует использовать напрямую, вместо этого приведение к void * в сочетании с static_cast:

T1 * p1=...
void *pv=p1;
T2 * p2= static_cast<T2*>(pv);

Вместо:

T1 * p1=...
T2 * p2= reinterpret_cast<T2*>(p1);

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

Заранее спасибо

p.s. Я знаю, для чего используется reinterpret_cast, но я никогда не видел, что используется таким образом

Ответы [ 3 ]

26 голосов
/ 08 декабря 2009

Для типов, для которых разрешено такое приведение (например, если T1 является POD-типом, а T2 - unsigned char), подход с static_cast четко определен Стандартом.

С другой стороны, reinterpret_cast полностью определяется реализацией - единственная гарантия, которую вы получите за это, - то, что вы можете привести тип указателя к любому другому типу указателя, а затем обратно, и вы получите исходное значение ; а также, вы можете привести тип указателя к целочисленному типу, достаточно большому, чтобы содержать значение указателя (которое варьируется в зависимости от реализации и вообще не должно существовать), а затем привести его обратно, и вы получите исходное значение.

Чтобы быть более конкретным, я просто процитирую соответствующие части Стандарта, выделив важные части:

5.2.10 [expr.reinterpret.cast]:

Отображение, выполняемое reinterpret_cast, определяется реализацией . [Примечание: он может или не может создавать представление, отличное от исходного значения.] ... Указатель на объект может быть явно преобразован в указатель на объект другого типа.) За исключением преобразования значения типа «Указатель на T1» на тип «указатель на T2» (где T1 и T2 являются типами объектов, а требования к выравниванию T2 не более строгие, чем требования для T1), и обратно к исходному типу возвращает исходное значение указателя, результат такого преобразования указателя не указан .

Так что-то вроде этого:

struct pod_t { int x; };
pod_t pod;
char* p = reinterpret_cast<char*>(&pod);
memset(p, 0, sizeof pod);

фактически не определено.

Объяснить, почему static_cast работает, немного сложнее. Вот вышеприведенный код, переписанный для использования static_cast, который, как я считаю, гарантированно всегда будет работать в соответствии со стандартом:

struct pod_t { int x; };
pod_t pod;
char* p = static_cast<char*>(static_cast<void*>(&pod));
memset(p, 0, sizeof pod);

Опять позвольте мне процитировать разделы стандарта, которые вместе приведут меня к выводу, что вышеприведенное должно быть переносимым:

3,9 [basic.types]:

Для любого объекта (кроме подобъекта базового класса) типа POD T, независимо от того, содержит ли объект допустимое значение типа T, базовые байты (1.7), составляющие объект, могут быть скопированы в массив char или unsigned char. Если содержимое массива char или unsigned char копируется обратно в объект, объект должен впоследствии сохранять свое первоначальное значение.

Представление объекта для объекта типа T представляет собой последовательность из N знаков без знака объектов , занятых объектом типа T, где N равно sizeof (T).

3.9.2 [basic.compound]:

Объекты cv-квалифицированного (3.9.3) или cv-неквалифицированного типа void* (указатель на void) могут использоваться для указания на объекты неизвестного типа. void* должен содержать любой указатель объекта. Квалифицированный по cv или неквалифицированный по cv (3.9.3) void* должен иметь те же требования к представлению и выравниванию, что и cv-квалифицированный или cv-неквалифицированный

3,10 [basic.lval]:

Если программа пытается получить доступ к сохраненному значению объекта через значение lvalue, отличное от одного из следующих типов, поведение не определено):

  • ...
  • тип char или unsigned char .

4,10 [conv.ptr]:

Значение типа «указатель на cv T», где T - тип объекта, может быть преобразовано в значение типа «указатель на cv void». Результат преобразования «указателя на cv T» в « указатель на cv void »указывает на начало места хранения, где находится объект типа T, как если бы этот объект был наиболее производным объектом (1.8) типа T (то есть не подобъектом базового класса).

5.2.9 [expr.static.cast]:

Инверсия любой стандартной последовательности преобразования (раздел 4), кроме преобразований lvalue-to-rvalue (4.1), array-topointer (4.2), function-to-pointer (4.3) и boolean (4.12), может быть выполнен явно с использованием static_cast.

[EDIT] С другой стороны, у нас есть этот драгоценный камень:

9.2 [class.mem] / 17:

Указатель на PОбъект OD-struct, соответствующим образом преобразованный с использованием reinterpret_cast, указывает на его начальный элемент (или, если этот элемент является битовым полем, затем на модуль, в котором он находится), и наоборот. [Примечание: может , поэтому может быть безымянным заполнением внутри объекта POD-struct, но не в его начале, что необходимо для достижения соответствующего выравнивания. ]

, что, по-видимому, означает, что reinterpret_cast между указателями так или иначе подразумевает "один и тот же адрес". Пойди разберись.

6 голосов
/ 13 декабря 2011

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

Обе формы будут работать на практике.

reinterpret_cast более четко описывает намерение и должно быть предпочтительным.

3 голосов
/ 08 декабря 2009

Настоящая причина этого в том, что C ++ определяет наследование и указатели на элементы.

С C указатель - это просто адрес, как и должно быть. В C ++ он должен быть более сложным из-за некоторых его особенностей.

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

Если вы многократно унаследовали два виртуальных объекта, которые также имеют некоторые конкретные части, это также катастрофа для стиля C. Это случай множественного наследования, который вызывает все проблемы, поэтому вам не следует использовать его в любом случае.

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

Единственный раз, когда я заканчиваю приведение, это когда примитивы в областях, которые С ++ решает, не совпадают, но где, очевидно, они должны быть. Что касается реальных объектов, то в любое время, когда вы захотите что-то привести, начните сомневаться в своем дизайне, потому что большую часть времени вы должны «программировать на интерфейс». Конечно, вы не можете изменить работу сторонних API, поэтому у вас не всегда есть большой выбор.

...