Кастинг через void * вместо использования reinterpret_cast

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

Ответ 1

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

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

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

5.2.10 [expr.reinterpret.cast]:

Отображение, выполненное с помощью reinterpret_cast, реализовано. [Примечание: оно может или не может выдавать представление, отличное от исходного значения.]... Указатель на объект может быть явно преобразован в указатель на объект другого типа.) За исключением того, что преобразование rvalue типа "указатель на 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 или без знака char. Если содержимое массива char или unsigned char будет скопировано обратно в объект, объект должен сохранить первоначальное значение.

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

3.9.2 [basic.compound]:

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

3.10 [basic.lval]:

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

  • ...
  • a char или неподписанный char тип.

4.10 [conv.ptr]:

Значение типа "указатель на cv T", где T - тип объекта, может быть преобразовано в rvalue типа "указатель на 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), функции-to-pointer (4.3) и boolean (4.12) может выполняться явно с использованием static_cast.

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

9.2 [class.mem]/17:

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

что, по-видимому, означает, что reinterpret_cast между указателями как-то подразумевает "тот же адрес". Идите фигуру.

Ответ 2

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

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

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

Ответ 3

Настоящая причина заключается в том, что С++ определяет наследование и из-за указателей элементов.

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

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

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

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

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