GrabDuck

Как работает aligned_storage() С++11

:

Давайте разберём указанную вами статью по абзацам.

Provides the member typedef type, which is a PODType...

Класс-обёртка std::aligned_storage является примитивным типом, то есть данные хранятся в нём непрерывной кучей байт (как структура в Си), без C++-заморочек. Благодаря этому его можно спокойно копировать хоть через memcpy.

... suitable for use as uninitialized storage...

Так как обёртка является примитивным типом, то у неё нет ни конструктора, ни деструктора. Соответственно, у оборачиваемого типа конструктор тоже вызван не будет. Как результат, мы можнем оборачивать только примитивные типы (увы).

... for any object whose size is at most Len and whose alignment requirement is a divisor of Align.

Оборачиваемый тип должен 1) полностью помещаться в обёртку и 2) его допустимое выравнивание (кратность адреса в оперативной памяти) должно быть делителем Align.

Возьмём следующий пример. Пусть Align = 16. Его делители — 1, 2, 4, 8 и 16. Это означает, что мы можем поместить в этот контейнер:

  • char/uint8_t/int8_t (выравнивание — 1 байт),
  • uint16_t/int16_t (выравнивание — 2 байта),
  • uint32_t/int32_t (выравнивание — 4 байта),
  • uint64_t/int64_t (выравнивание — 8 байт),
  • либо SSE вектор, представленный в виде uint8_t[16] (ожидаемое выравнивание — 16 байт).

Зачем было введено выравнивание? Процессор обменивается с ОЗУ не байтами, а блоками фиксированного размера 2n при неком целом n. Если переменная выровнена, то она может быть передана за наименее возможное количество пересылок. Невыровненные же данные могут пересечь границу блока и потребовать на одну пересылку больше.

В придачу, некоторые операции (команды быстрой выровненной загрузки в SSE, например), вообще выбрасывают прерывание процессора при отсутствии выравнивания на определённую границу (для SSE — 16 байт).

Стоит отметить, что выравнивание равно размеру только для примитивных типов. У составных же типов (структур и объединений) выравнивание равно таковому у наибольшего поля. Это гарантирует, что наиболее длинное поле будет выровнено по своему размеру. Ну а так как размеры примитивных типов равны степени двойки (то есть выравнивание больших типов кратно выравниванию типов меньших), то и все остальные поля будут выровнены.

The default value of Align is the most stringent (the largest) alignment requirement for any object whose size is at most Len.

Align можно и не указывать. Тогда компилятор будет выравнивать так, как он бы выравнивал близжайший (но не превышающий) по размерам тип. То есть будто мы написали std::aligned_storage<sizeof(T), alignof(T)>, где T — некий встроенный (пусть и не существующий) тип с нужным нам размером.

If the default value is not used, Align must be the value of alignof(T) for some type T, or the behavior is undefined.

Третий фрагмент текста перечислял типы, допустимые для размещения в конкретной специализации обёртки с конкретным размером и выравниванием. Этот же абзац налагает ограничение на конкретный выбранный тип: Align должен иметь типичное для данного типа выравнивание. То есть мы не можем разместить uint16_t с Align=6, полагая, что 6 и 3 — допустимые значения выравнивания, — это провоцирует неопределённое поведение (выше уже был приведён пример про выравнивание для SSE).

Но данный фрагмент также запрещает задание Align, кратного настоящему выравниванию. К примеру, мы не можем разместить uint16_t с выравниванием в 4, 8, 12 и т. д. байт.

И да, мы не можем выравнивать SSE-вектор, если у нас нет готового, 16-байтного типа. Ведь тогда мы вынуждены использовать char[16], чьё родное выравнивание — 1 байт (т. к. тип char). Нам же нужно 16 — здравствуй, неопределённое поведение? А будь у нас готовый тип — компилятор сам его выровняет по собственному размеру (который совпадает с требуемым выравниванием), и std::aligned_storage нам как бы и не нужен.

The behavior is undefined if Len == 0.

Обёртка должна иметь ненулевую длину (иначе в неё ничего не поместится).