Вариация на тему типа прокалывания: на месте тривиальная конструкция

9

Я знаю, что это довольно распространенная тема, но насколько легко найти типичный 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)

spectras
источник
2
Но Pixelновые смежные s все еще не являются массивом Pixels.
Jarod42
1
@spectras Это не делает массив, хотя. У вас просто куча объектов Pixel рядом друг с другом. Это отличается от массива.
Натан Оливер
1
Так нет, где ты делаешь pixels[some_index]или *(pixels + something)? Это было бы UB.
Натан Оливер
1
Соответствующий раздел здесь, а ключевая фраза - если P указывает на элемент массива i объекта массива x . Здесь pixels(P) не указатель на объект массива, а указатель на одиночный объект Pixel. Это означает, что вы можете получить доступ только на pixels[0]законных основаниях.
Натан Оливер
3
Вы хотите прочитать wg21.link/P0593 .
Ecatmur

Ответы:

3

Неопределенное поведение использовать результат promoteв качестве массива. Если мы посмотрим на [expr.add] /4.2 мы имеем

В противном случае, если Pуказывает на элемент массива iобъекта массиваx с nэлементами ([dcl.array]), выражения P + Jи J + P(где Jимеет значение j) указывает на (возможно, гипотетический) элементу массива i+jиз , xесли это 0≤i+j≤nи выражение P - Jуказывает на ( возможно , -гипотетический) элемент массива i−jиз xесли 0≤i−j≤n.

мы видим, что для этого требуется указатель, чтобы фактически указывать на объект массива. На самом деле у вас нет объекта массива. У вас есть указатель на сингл, Pixelкоторый случайно оказался рядом с другим Pixelsв непрерывной памяти. Это означает, что единственный элемент, к которому вы можете получить доступ, это первый элемент. Попытка получить доступ ко всему остальному была бы неопределенным поведением, потому что вы вышли за пределы допустимого домена для указателя.

NathanOliver
источник
Спасибо за быстрое выяснение. Я сделаю итератор вместо этого, я думаю. В качестве обозначения это также означает, что &somevector[0] + 1это UB (ну, я имею в виду, использование результирующего указателя будет).
спектры
@spectras На самом деле все в порядке. Вы всегда можете получить указатель на один объект. Вы просто не можете разыменовать этот указатель, даже если там есть допустимый объект.
Натан Оливер
Да, я отредактировал комментарий, чтобы сделать его более понятным, я имел в виду разыменование результирующего указателя :) Спасибо за подтверждение.
спектры
@spectras Нет проблем. Эта часть C ++ может быть очень сложной. Хотя аппаратное обеспечение будет делать то, что нам нужно, на самом деле это не то, для чего мы кодировали. Мы пишем код на абстрактной машине C ++, и это очень привередливая машина;) Надеюсь, P0593 будет принят, и это станет намного проще.
Натан Оливер
1
@spectras Нет, потому что вектор std определен как содержащий массив, и вы можете выполнять арифметику указателей между элементами массива. К сожалению, нет способа реализовать вектор std в самом C ++ без использования UB.
Якк - Адам Невраумонт
1

У вас уже есть ответ относительно ограниченного использования возвращаемого указателя, но я хочу добавить, что я также думаю, что вам нужно 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объектов.

грецкий орех
источник
Я считаю, что это правильно. Даже если массив вещь (unimplementability из std::vector) не является проблемой, вы все равно должны std::launderрезультата перед обращением к любой из таргетингом newред Pixels. На данный момент, std::launderэто UB, так как соседние Pixels будут доступны из отмытого указателя.
Fureeish
@Fureeish Я не уверен, почему std::launderбудет UB, если применяется resultдо возвращения. Прилегающий Pixelне « достижим » через отмытый указатель происходит мое понимание eel.is/c++draft/ptr.launder#4 . И даже это было, я не вижу, как это UB, потому что весь исходный std::byteмассив доступен из исходного указателя.
грецкий орех
Но следующий Pixelне будет доступен из std::byteуказателя, но это из launderуказателя ed. Я считаю, что это актуально здесь. Я счастлив быть исправленным, все же.
Фуреиш
@Fureeish Из того, что я могу сказать, ни один из приведенных примеров не применим здесь, и определение требования также говорит то же самое, что и стандарт. Достижимость определяется в байтах, а не в объектах. Байт, занятый следующим, Pixelкажется мне доступным из исходного указателя, потому что исходный указатель указывает на элемент std::byteмассива, который содержит байты, составляющие хранилище для Pixelсоздания " или внутри непосредственно включающего массива, в котором Z является Элемент "условие применяется (где Zесть Y, то есть сам std::byteэлемент).
грецкий орех
Я думаю, что байты памяти, которые Pixelзанимают следующие , не достижимы через отмытый указатель, потому что указанный Pixelобъект не является элементом объекта массива и также не может быть преобразован в указатель с любым другим соответствующим объектом. Но я также думаю об этой детали std::launderвпервые в этой глубине. Я не уверен на 100% в этом.
грецкий орех