Следующий фрагмент кода имеет неопределенное поведение, поскольку время жизни данных не запускается.
#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 и нет неопределенного поведения? Если да, когда в этом примере закончится срок службы?
If the objects are potentially-overlapping or not TriviallyCopyable, the behavior of memmove is not specified and may be undefined.
@MarekR Но ни то, ни другое не применимо к примеру ОП, как написано.
ТЛ;ДР
В общем, это по-прежнему 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 user17732522 Я предполагал, что в реальном коде Data
не пустая структура. Возможно, ОП должен внести ясность.
@ user17732522 AFAIK, alignas
нельзя использовать напрямую для выравнивания buf
, вам придется обернуть его внутри struct
, выровненного по alignas
.
Если Data
не пуст и конструктор копирования действительно копирует некоторые члены, то это тоже будет UB независимо от того, определен ли сам memmove
и отмыватель, поскольку он будет читать неопределенное значение. @OP Если ваш тип не такой, как написано, он может легко усложниться.
Почему alignas
на самом массиве не должно работать? В целом это не очень хорошее решение, потому что выравнивание не будет распространяться на тип, но в противном случае я не понимаю, почему это не должно работать.
@user17732522 user17732522, моя вина. Какой синтаксис будет правильным: alignas(alignof(Data)) char buf[sizeof(Data)]
или char buf alignas(alignof(Data)) [sizeof(Data)]
?
Достаточно первого и alignas(Data)
(сокращение от alignas(alignof(Data))
).
Если OP имеет в виду что-то более сложное, им также следует быть осторожными, чтобы new(buf) Data
не передавал новому объекту значение, соответствующее исходным байтам в массиве, даже если конструктор по умолчанию тривиален. Если они этого хотят, им следует вместо этого использовать std::start_lifetime_as
.
@user17732522 user17732522 В C++26 этот конструктор копирования больше не будет UB, но в C++26 у вас также есть std::start_lifetime_as
Стандарт наделяет буферы char
и unsigned char
разными свойствами. Мне непонятно, когда можно использовать только unsigned char
(или std::byte
). Если кто-то может уточнить и сказать мне, нужно ли мне исправить свой ответ... (Я думаю, что разница заключается в типах массивов, которые можно использовать для хранения объектов, и типах указателей, которые можно использовать для ссылки на местоположения этих объектов. ).
@Artyer Однако в C++26 он (как правило) по-прежнему будет вести себя ошибочно, поэтому он все равно может выйти из строя во время выполнения.
«если у вас есть допустимое представление объекта внутри буфера»: в этом нет необходимости. В противном случае значение объекта будет просто не указано. Но в любом случае это уже (более или менее) так, потому что OP вообще не инициализирует буфер или объект. Конечно, и это относится и к использованию Placement-New, если какой-либо подобъект будет иметь неопределенное, неопределенное или ошибочное значение, то копия в функции может вести себя не так, как ожидалось. (Смотрите обсуждение в предыдущих комментариях.)
@Привел пример этого утверждения, «за исключением операций постоянной оценки (начиная с C++26), которые начинают жизнь массива типа unsigned char или std::byte(начиная с C++17), и в этом случае такие объекты созданный в массиве",?
Я мог бы сказать, что во втором блоке кода есть три потенциальных неопределенных поведения:
1. memmove
специально разрешает перекрытие диапазонов. В этом и разница с memcpy
. 2. Если указатель указывает на тип с указанием const
, то он не скомпилируется. 3. reinterpret_cast
также не будет компилироваться, если указатель относится к const
-квалифицированному типу. Фактически вызов функции уже не скомпилируется. Если вы имеете в виду случай, когда пользователь явно объявляет массив const
, а затем отбрасывает const
с помощью const_cast
, чтобы передать массив функции, то это ответственность вызывающей стороны.
Обратите внимание, что первый пример (при условии, что
alignof(Data) == 1
) имеет только неопределенное поведение, поскольку массив имеет типchar
. Если бы он был типаunsigned char
илиstd::byte
, то начало существования массива также уже неявно создавало бы объекты.