C++ Неопределенное поведение при использовании переинтерпретации приведения

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

#include <iostream>
#include <cstring>
struct Data{

};
void process(char * buff){
    decltype(auto) d = std::launder(reinterpret_cast<Data *>(buff));
}
int main() {
     char buf[sizeof(Data)];
     process(buf);
}

Чтение cppreference memmove говорит следующее:

std::memmove может использоваться для неявного создания объектов в буфер назначения.

Мой вопрос в том, изменен ли приведенный выше фрагмент кода на

#include <iostream>
#include <cstring>
struct Data{

};
void process(char * const buff){
    decltype(auto) d = std::launder(reinterpret_cast<Data *>(std::memmove(buff,buff,sizeof(Data))));
}
int main() {
   char buf[sizeof(Data)];
   process(buf);
}

Запускается ли время жизни данных неявно во время memmove и нет неопределенного поведения? Если да, когда в этом примере закончится срок службы?

Обратите внимание, что первый пример (при условии, что alignof(Data) == 1) имеет только неопределенное поведение, поскольку массив имеет тип char. Если бы он был типа unsigned char или std::byte, то начало существования массива также уже неявно создавало бы объекты.

user17732522 28.08.2024 15:07
std::memmoveIf the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memmove is not specified and may be undefined.
Marek R 28.08.2024 15:28

@MarekR Но ни то, ни другое не применимо к примеру ОП, как написано.

user17732522 28.08.2024 15:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
93
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

ТЛ;ДР
В общем, это по-прежнему UB, поскольку вы не перемещаете представление объекта из надлежащего объекта в целевой буфер.


Возможным исключением является случай, когда Data имеет неявное время жизни (обратите внимание, это касается пустого struct, как в примере с вопросом).
Вам придется изменить объявление буфера на unsigned char buf[sizeof(Data)] или std::byte buf[sizeof(Data)] и соответствующим образом изменить подпись process.
В этом случае memmove также не требуется, поскольку создания массива байтов достаточно, чтобы начать жизнь таких объектов: см. здесь.
Обратите внимание, что в этом случае Data все еще не инициализирован.

В противном случае вы можете разместить новое размещение на buf:

int main() {
   unsigned char buf[sizeof(Data)];
   new(buf) Data; // creates a Data object at buf, be careful of alignment
   // here I'm using default constructor, other constructors can be used.
   process(buf); // don't need memmove there
}

Обратите внимание, что объектное представление нового объекта Data не связано ни с каким значением, ранее хранившимся в буфере. Это будет зависеть от используемого конструктора.

Кроме того, как упоминалось во фрагменте, будьте осторожны с выравниванием. Если данные перевыровнены, вы все равно являетесь UB, иначе вам придется использовать буфер большего размера и std::align (или более простое решение, см. ниже).

В любом случае такая конструкция опасна, поскольку изнутри process у вас нет возможности быть уверенным, что объект Data действительно находится в buf.


Благодаря комментарию @user17732522, чтобы получить лучшие гарантии выравнивания, можно было бы:

alignas(Data) unsigned char buf[sizeof(Data)];

Благодаря комментарию @user17732522 другим вариантом может быть std::start_lifetime_as из C++23, если у вас есть допустимое представление объекта внутри буфера (получение такого представления — это другая история).

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

Data — неявное время жизни. Так что это не проблема. «Если данные слишком выровнены, вы все еще UB»: это уже проблема, если alignof(Data) != 1. Не обязательно иметь чрезмерное выравнивание, чтобы вызвать проблемы с выравниванием. std::align и буфер большого размера не нужен. Буфер можно правильно выровнять с помощью спецификатора alignas.
user17732522 28.08.2024 15:09

@user17732522 user17732522 Я предполагал, что в реальном коде Data не пустая структура. Возможно, ОП должен внести ясность.

Oersted 28.08.2024 15:11

@ user17732522 AFAIK, alignas нельзя использовать напрямую для выравнивания buf, вам придется обернуть его внутри struct, выровненного по alignas.

Oersted 28.08.2024 15:13

Если Data не пуст и конструктор копирования действительно копирует некоторые члены, то это тоже будет UB независимо от того, определен ли сам memmove и отмыватель, поскольку он будет читать неопределенное значение. @OP Если ваш тип не такой, как написано, он может легко усложниться.

user17732522 28.08.2024 15:14

Почему alignas на самом массиве не должно работать? В целом это не очень хорошее решение, потому что выравнивание не будет распространяться на тип, но в противном случае я не понимаю, почему это не должно работать.

user17732522 28.08.2024 15:17

@user17732522 user17732522, моя вина. Какой синтаксис будет правильным: alignas(alignof(Data)) char buf[sizeof(Data)] или char buf alignas(alignof(Data)) [sizeof(Data)]?

Oersted 28.08.2024 15:28

Достаточно первого и alignas(Data) (сокращение от alignas(alignof(Data))).

user17732522 28.08.2024 15:29

Если OP имеет в виду что-то более сложное, им также следует быть осторожными, чтобы new(buf) Data не передавал новому объекту значение, соответствующее исходным байтам в массиве, даже если конструктор по умолчанию тривиален. Если они этого хотят, им следует вместо этого использовать std::start_lifetime_as.

user17732522 28.08.2024 15:45

@user17732522 user17732522 В C++26 этот конструктор копирования больше не будет UB, но в C++26 у вас также есть std::start_lifetime_as

Artyer 28.08.2024 16:33

Стандарт наделяет буферы char и unsigned char разными свойствами. Мне непонятно, когда можно использовать только unsigned char (или std::byte). Если кто-то может уточнить и сказать мне, нужно ли мне исправить свой ответ... (Я думаю, что разница заключается в типах массивов, которые можно использовать для хранения объектов, и типах указателей, которые можно использовать для ссылки на местоположения этих объектов. ).

Oersted 28.08.2024 16:44

@Artyer Однако в C++26 он (как правило) по-прежнему будет вести себя ошибочно, поэтому он все равно может выйти из строя во время выполнения.

user17732522 28.08.2024 18:43

«если у вас есть допустимое представление объекта внутри буфера»: в этом нет необходимости. В противном случае значение объекта будет просто не указано. Но в любом случае это уже (более или менее) так, потому что OP вообще не инициализирует буфер или объект. Конечно, и это относится и к использованию Placement-New, если какой-либо подобъект будет иметь неопределенное, неопределенное или ошибочное значение, то копия в функции может вести себя не так, как ожидалось. (Смотрите обсуждение в предыдущих комментариях.)

user17732522 28.08.2024 18:45

@Привел пример этого утверждения, «за исключением операций постоянной оценки (начиная с C++26), которые начинают жизнь массива типа unsigned char или std::byte(начиная с C++17), и в этом случае такие объекты созданный в массиве",?

getsoubl 29.08.2024 08:11

Я мог бы сказать, что во втором блоке кода есть три потенциальных неопределенных поведения:

  1. Поведение memmove в случае перекрытия источника и пункт назначения не указан и может быть неопределенным.
  2. Назначением memmove является указатель на const, что приведет к неопределенное поведение.
  3. Аргумент функции теперь является указателем на константа, и вы пытаетесь отбросить константность, используя reinterpret_cast, что также является неопределенным поведением.

1. memmove специально разрешает перекрытие диапазонов. В этом и разница с memcpy. 2. Если указатель указывает на тип с указанием const, то он не скомпилируется. 3. reinterpret_cast также не будет компилироваться, если указатель относится к const-квалифицированному типу. Фактически вызов функции уже не скомпилируется. Если вы имеете в виду случай, когда пользователь явно объявляет массив const, а затем отбрасывает const с помощью const_cast, чтобы передать массив функции, то это ответственность вызывающей стороны.

user17732522 28.08.2024 15:33

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

Похожие вопросы