Я знаю, что это довольно распространенная тема, но насколько легко найти типичный UB, я не нашел этот вариант до сих пор.
Итак, я пытаюсь официально представить объекты Pixel, избегая при этом фактической копии данных.
Это действительно?
struct Pixel {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
};
static_assert(std::is_trivial_v<Pixel>);
Pixel* promote(std::byte* data, std::size_t count)
{
Pixel * const result = reinterpret_cast<Pixel*>(data);
while (count-- > 0) {
new (data) Pixel{
std::to_integer<uint8_t>(data[0]),
std::to_integer<uint8_t>(data[1]),
std::to_integer<uint8_t>(data[2]),
std::to_integer<uint8_t>(data[3])
};
data += sizeof(Pixel);
}
return result; // throw in a std::launder? I believe it is not mandatory here.
}
Ожидаемая схема использования, сильно упрощенная:
std::byte * buffer = getSomeImageData();
auto pixels = promote(buffer, 800*600);
// manipulate pixel data
Более конкретно:
- Этот код имеет четко определенное поведение?
- Если да, то безопасно ли использовать возвращенный указатель?
- Если да, то на какие другие
Pixel
типы он может быть распространен? (ослабление ограничения is_trivial «пиксель только с 3 компонентами»).
И clang, и gcc оптимизируют весь цикл до небытия, чего я и хочу. Теперь я хотел бы знать, нарушает ли это некоторые правила C ++ или нет.
Ссылка Godbolt, если вы хотите поиграть с ней.
(примечание: я не пометил c ++ 17, несмотря на то std::byte
, что вопрос касается использования char
)
Pixel
новые смежные s все еще не являются массивомPixel
s.pixels[some_index]
или*(pixels + something)
? Это было бы UB.pixels
(P) не указатель на объект массива, а указатель на одиночный объектPixel
. Это означает, что вы можете получить доступ только наpixels[0]
законных основаниях.Ответы:
Неопределенное поведение использовать результат
promote
в качестве массива. Если мы посмотрим на [expr.add] /4.2 мы имееммы видим, что для этого требуется указатель, чтобы фактически указывать на объект массива. На самом деле у вас нет объекта массива. У вас есть указатель на сингл,
Pixel
который случайно оказался рядом с другимPixels
в непрерывной памяти. Это означает, что единственный элемент, к которому вы можете получить доступ, это первый элемент. Попытка получить доступ ко всему остальному была бы неопределенным поведением, потому что вы вышли за пределы допустимого домена для указателя.источник
&somevector[0] + 1
это UB (ну, я имею в виду, использование результирующего указателя будет).У вас уже есть ответ относительно ограниченного использования возвращаемого указателя, но я хочу добавить, что я также думаю, что вам нужно
std::launder
даже иметь доступ к первомуPixel
:reinterpret_cast
Делается до того , какPixel
создается объект (если вы не сделаете это вgetSomeImageData
). Поэтомуreinterpret_cast
не изменит значение указателя. Полученный указатель все равно будет указывать на первый элементstd::byte
массива, переданный функции.Когда вы создаете
Pixel
объекты, они будут вложены вstd::byte
массив, иstd::byte
массив будет предоставлять хранилище дляPixel
объектов.В некоторых случаях повторное использование хранилища приводит к тому, что указатель на старый объект автоматически указывает на новый объект. Но это не то, что здесь происходит, поэтому
result
все равно будет указывать наstd::byte
объект, а не наPixel
объект. Я предполагаю, что использование его так, как если бы оно указывало наPixel
объект, технически будет неопределенным поведением.Я думаю, что это все еще сохраняется, даже если вы делаете
reinterpret_cast
после созданияPixel
объекта, так какPixel
объект и то,std::byte
что обеспечивает хранение для него, не взаимозаменяемы с указателем . Так что даже тогда указатель будет продолжать указыватьstd::byte
наPixel
объект , а не на объект.Если вы получили указатель для возврата из результата одного из новых объектов размещения, то все должно быть в порядке, если речь
Pixel
идет о доступе к этому конкретному объекту.Также вам нужно убедиться, что
std::byte
указатель правильно выровненPixel
и массив действительно достаточно большой. Насколько я помню, стандарт на самом деле не требует, чтобы онPixel
имел такое же выравниваниеstd::byte
или не имел отступов.Также ничто из этого не зависит от того,
Pixel
является ли оно тривиальным или действительно каким-либо другим его свойством. Все будет вести себя одинаково, покаstd::byte
массив имеет достаточный размер и соответствующим образом выровнен дляPixel
объектов.источник
std::vector
) не является проблемой, вы все равно должныstd::launder
результата перед обращением к любой из таргетингомnew
редPixel
s. На данный момент,std::launder
это UB, так как соседниеPixel
s будут доступны из отмытого указателя.std::launder
будет UB, если применяетсяresult
до возвращения. ПрилегающийPixel
не « достижим » через отмытый указатель происходит мое понимание eel.is/c++draft/ptr.launder#4 . И даже это было, я не вижу, как это UB, потому что весь исходныйstd::byte
массив доступен из исходного указателя.Pixel
не будет доступен изstd::byte
указателя, но это изlaunder
указателя ed. Я считаю, что это актуально здесь. Я счастлив быть исправленным, все же.Pixel
кажется мне доступным из исходного указателя, потому что исходный указатель указывает на элементstd::byte
массива, который содержит байты, составляющие хранилище дляPixel
создания " или внутри непосредственно включающего массива, в котором Z является Элемент "условие применяется (гдеZ
естьY
, то есть самstd::byte
элемент).Pixel
занимают следующие , не достижимы через отмытый указатель, потому что указанныйPixel
объект не является элементом объекта массива и также не может быть преобразован в указатель с любым другим соответствующим объектом. Но я также думаю об этой деталиstd::launder
впервые в этой глубине. Я не уверен на 100% в этом.