Нужно ли использовать std :: launder при выполнении арифметики с указателями в объекте стандартной компоновки (например, с помощью offsetof)?
Этот вопрос является продолжением: добавляет ли UB к указателю "char *", когда он на самом деле не указывает на массив символов?
В CWG 1314 CWG подтвердил, что допустимо выполнять арифметику указателя в объекте стандартной компоновки с использованием указателя unsigned char
. Казалось бы, это подразумевает, что некоторый код, подобный коду в связанном вопросе, должен работать как предполагалось:
struct Foo {
float x, y, z;
};
Foo f;
unsigned char *p = reinterpret_cast<unsigned char*>(&f) + offsetof(Foo, z); // (*)
*reinterpret_cast<float*>(p) = 42.0f;
(Я заменил char
на unsigned char
для большей ясности.)
Однако, похоже, что новые изменения в С++ 17 подразумевают, что этот код теперь UB, если только std::launder
не используется после обоих reinterpret_cast
s. Результат reinterpret_cast
между двумя типами указателей эквивалентен двум static_cast
: первый cv void*
, второй тип указателя назначения. Но [expr.static.cast]/13 подразумевает, что это создает указатель на исходный объект, а не на объект типа назначения, поскольку объект типа Foo
не взаимозаменяем с указателем с unsigned char
объектом unsigned char
в его первом байте Кроме того, объект unsigned char
в первом байте fz
указателя взаимно fz
самим fz
.
Мне трудно поверить, что комитет намеревался изменить ситуацию, которая бы нарушила эту очень распространенную идиому, сделав все использования offsetof
до С++ 17 offsetof
неопределенными.
Ответы
Ответ 1
Первое приведение не является неопределенным поведением и следует из правил псевдонимов типов:
Тип псевдонимов
Всякий раз, когда делается попытка прочитать или изменить сохраненное значение объекта типа DynamicType с помощью glvalue типа AliasedType, поведение не определено, если не выполнено одно из следующих условий:
AliasedType и DynamicType похожи.
AliasedType является (возможно, cv-квалифицированным) подписанным или неподписанным вариантом DynamicType.
AliasedType имеет тип std :: byte, (начиная с С++ 17) char или unsigned char: это позволяет исследовать объектное представление любого объекта в виде массива байтов.
Так что это делает первый бросок ОК
//OK, can examine through p
unsigned char *p = reinterpret_cast<unsigned char*>(&f) + offsetof(Foo, z);
Собственно говоря, следующий актерский состав и дереф. небезопасно и поэтому может привести к неопределенному поведению, поскольку указываемые на типы не похожи (или одно из других условий):
*reinterpret_cast<float*>(p) = 42.0f;
Это не имеет ничего общего с std::launder
- или, другими словами, это приведение уже было небезопасным до c++17 (в отличие от того, что вы намекаете).
Обновление: причина, по которой этот последний случай считается небезопасным, также связана с правилами псевдонимов типов. То есть unsigned char*
не похож на float*
(и ни один из других случаев не имеет места) - (неофициально similiar близок к тому же типу, учитывая некоторые вариации const volatile
).
Тип псевдонимов вступает в игру из-за этого случая reinterpret_cast
:
Любой указатель объекта типа T1 * может быть преобразован в другой указатель объекта типа cv T2 *. Это в точности эквивалентно static_cast <cv T2 *> (static_cast (expression)) (что подразумевает, что если требование выравнивания T2 не является более строгим, чем у T1, значение указателя не изменяется, и преобразование полученного указателя обратно в исходный тип дает исходное значение). В любом случае результирующий указатель может быть безопасно разыменован, только если это разрешено правилами псевдонимов типов (см. Ниже)
В последнем преобразовании OP разыменовывает указатель после такого преобразования, поэтому его можно назвать небезопасным.
Ответ 2
Ваш вопрос был:
Нужно ли использовать std :: launder при выполнении арифметики с указателями в объекте стандартной компоновки (например, с помощью offsetof)?
Нет.
Использование только стандартного типа макета не может привести к ситуации, когда вам когда-либо потребуется использовать std :: launder.
Пример можно немного упростить: просто используйте целочисленный тип для хранения адреса вместо unsigned char*
.
uintptr_t
этого используйте uintptr_t
:
struct Foo {
float x, y, z;
};
static_assert(std::is_standard_layout<Foo>::value);
Foo f;
uintptr_t addr = reinterpret_cast<uintptr_t>(&f) + offsetof(Foo, z);//OK: storing addr as integer type instead
//uintptr_t addr = reinterpret_cast<uintptr_t>(&f.z);//equivalent: ei. guarenteed to yield same address!
*reinterpret_cast<float*>(addr) = 42.0f;
Пример теперь очень прост - больше нет преобразования в unsigned char*
и мы просто получаем адрес и возвращаемся к исходному типу указателя.. (Вы также подразумеваете, что это не работает?)
std::launder
обычно просто необходим в подмножестве случаев (например, из-за члена const), когда вы изменяете (или создаете) базовый объект некоторым способом выполнения (например, посредством размещения new). Мнемоника: объект "грязный" и должен иметь std::launder
.