Варіація на тему вибору типу: тривіальна побудова на місці


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)


2
Але суміжний Pixelрозміщений новий все ще не є масивом Pixels.
Jarod42

1
@spectras Хоча це не масив. Ви просто маєте купу об’єктів Pixel поруч. Це відрізняється від масиву.
NathanOliver

1
Так ні де ти робиш pixels[some_index]чи *(pixels + something)? Це був би UB.
NathanOliver

1
Відповідний розділ тут і ключова фраза , якщо P вказує на елемент масиву I від об'єкта масиву х . Тут pixels(P) - не вказівник на об’єкт масиву, а вказівник на єдиний Pixel. Це означає, що ви можете отримати доступ лише pixels[0]легально.
NathanOliver

3
Ви хочете прочитати wg21.link/P0593 .
екатмур

Відповіді:


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слідують за ним у суміжній пам'яті. Це означає, що єдиний елемент, до якого ви можете фактично отримати доступ, це перший елемент. Спроба отримати доступ до чого-небудь іншого буде не визначеною поведінкою, оскільки ви минули в кінці дійсного домену для вказівника.


Дякуємо, що дізналися це швидко. Натомість я зроблю ітератор. Як сторонне позначення, це також означає, що &somevector[0] + 1це UB (ну, я маю на увазі, використовуючи отриманий вказівник було б).
спектри

@spectras Це насправді добре. Ви завжди можете отримати вказівник на один минулий об’єкт. Ви просто не можете перенаправити цей покажчик, навіть якщо там є дійсний об'єкт.
NathanOliver

Так, я відредагував коментар, щоб зробити себе більш зрозумілим, я мав на увазі скасування результату вказівника :) Дякую за підтвердження.
спектри

@spectras Немає проблем. Ця частина C ++ може бути дуже складною. Незважаючи на те, що обладнання буде робити те, що ми хочемо, це насправді не те, що було кодуванням. Ми кодуємо абстрактну машину C ++, і це машина, яка є персонажем;) Сподіваємось, P0593 буде прийнятий, і це стане набагато простіше.
NathanOliver

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

@Fureeish З того, що я можу сказати, жоден із наведених прикладів не застосовується тут, і визначення вимоги також говорить те саме, що і стандарт. Доступність визначається в байтах зберігання, а не в об'єктах. Байт, зайнятий наступним, Pixelздається мені доступним з оригінального вказівника, тому що оригінальний покажчик вказує на елемент std::byteмасиву, який містить байти, що складають сховище для Pixelстворення " або в межах безпосередньо огороджувального масиву, з якого Z є застосовується умова елемента (де Zє Y, тобто сам std::byteелемент).
волоський горіх

Я думаю, що байти сховища, які Pixelзаймають наступні , не є доступними через відмитий покажчик, тому що Pixelоб'єкт із загостреним об'єктом не є елементом масиву об'єкта, а також не вказує на взаємоперетворення з будь-яким іншим відповідним об'єктом. Але я також std::launderвперше замислююся над цією деталлю в цій глибині. Я також не на 100% впевнений у цьому.
волоський горіх
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.