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

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

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

Вместо:

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

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

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

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

3 ответа

Решение

Для типов, для которых разрешено такое приведение (например, если 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 или unsigned char копируется обратно в объект, объект должен впоследствии сохранять свое первоначальное значение.

Объектное представление объекта типа T - это последовательность из N беззнаковых объектов char, занятых объектом типа 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-неквалифицированные char*,

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-inter (4.3) и boolean (4.12) явно используя static_cast.

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

9.2 [class.mem] / 17:

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам