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

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