align_storage и строгие псевдонимы

В настоящее время я используюalign_storage для реализации типа «Необязательный», аналогичный типу boost:: необязательный. Для этого у меня есть такой член класса:

typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type t_;

Я использую новое размещение для создания объекта, однако я нигде не сохраняю возвращаемый указатель. Вместо этого я обращаюсь к базовому типу объекта во всех моих функциях-членах, подобных этому (очевидно, с проверками, чтобы убедиться, что объект действителен с помощью логического флага, также хранящегося в моем необязательном типе):

T const* operator->() const {
    return static_cast<T const*>(static_cast<void const*>(&t_));
}

Мой вопрос в том, безопасно ли это. Насколько я понимаю, мое использование размещения new изменяет «динамический тип» объекта, и пока я продолжаю обращаться к памяти, используя этот тип, со мной все будет в порядке. Однако мне не ясно, должен ли я удерживать указатель, возвращенный из нового размещения, или мне разрешено просто приводить к базовому типу всякий раз, когда мне нужно получить к нему доступ. Я прочитал раздел 3.10 стандарта С++ 11, однако я недостаточно свободно говорю на стандартном языке, чтобы быть уверенным.

Если возможно, я бы чувствовал себя лучше, если бы вы могли сослаться на стандарт в своем ответе (это помогает мне спать по ночам: P).


person RaptorFactor    schedule 20.11.2012    source источник
comment
Я думаю, что это сводится к тому, может ли указатель, возвращаемый оператором new, отличаться от адреса результирующего объекта. Раздел 5.3.4.14 стандарта C++03 указывает, что он не обязательно будет таким же, если объект является массивом, что наводит меня на мысль, что в противном случае он будет таким же.   -  person Vaughn Cato    schedule 20.11.2012
comment
В конечном счете, реализация определяется тем, является ли базовый адрес выделения первым байтом пространства, занимаемого объектом. Поэтому правильнее брать результат new. Очевидным случаем является выделение через new[]; Реализации часто хранят информацию, необходимую для уничтожения объектов массива, в первых байтах распределения.   -  person justin    schedule 20.11.2012
comment
Обратите внимание, что в С++ 17 есть std::optional...   -  person Macmade    schedule 31.01.2018


Ответы (1)


Использование ABICT безопасно.

  • Размещение new объекта типа T создаст объект, начиная с переданного адреса.

§5.3.4/10 говорит:

Новое выражение передает объем запрошенного пространства функции распределения в качестве первого аргумента типа std::size_t. Этот аргумент должен быть не меньше размера создаваемого объекта; он может быть больше размера создаваемого объекта, только если объект является массивом.

Для объекта, не являющегося массивом, выделенный размер не может превышать размер объекта, поэтому представление объекта должно начинаться с начала выделенной памяти, чтобы соответствовать.

Размещение new возвращает переданный указатель (см. § 18.6.1.3/2) в результате «распределения», поэтому объектное представление созданного объекта начнется с этого адреса.

  • static_cast<> и неявные преобразования между типом T* и void* преобразуют между указателем на объект и указателем на его хранилище, если объект является полным объектом.

§4.10/2 говорит:

Значение prvalue типа «указатель на cv T», где T — тип объекта, может быть преобразовано в значение prvalue типа «указатель на cv void». Результат преобразования «указателя на cv T» в «указатель на cv void» указывает на начало места хранения, где находится объект типа T, как если бы объект был наиболее производным объектом (1.8) типа T. [...]

Это определяет неявное преобразование для преобразования, как указано. Далее §5.2.9[expr.static.cast]/4 определяет static_cast<> для явных преобразований, где неявное преобразование имеет тот же эффект, что и неявное преобразование:

В противном случае выражение e можно явно преобразовать в тип T, используя static_cast формы static_cast<T>(e), если объявление T t(e); правильно построено, для некоторой изобретенной временной переменной t (8.5). Эффект от такого явного преобразования такой же, как при выполнении объявления и инициализации с последующим использованием временной переменной в результате преобразования. [...]

Для обратного static_cast<> (от void* до T*) в §5.2.9/13 говорится:

Значение prvalue типа «указатель на cv1 void» может быть преобразовано в значение prvalue типа «указатель на cv2 T», где T — тип объекта, а cv2 — такое же или большее значение cv-квалификации, чем cv1. [...] Значение типа указатель на объект, преобразованное в «указатель на cv void» и обратно, возможно, с другой квалификацией cv, должно иметь исходное значение.

Таким образом, если у вас есть void*, указывающий на хранилище объекта T (которое является значением указателя, которое будет результатом неявного преобразования T* в объект, то static_cast этого в T* создаст действительный указатель на объект .

Возвращаясь к вашему вопросу, предыдущие пункты подразумевают, что если у вас есть

typename std::aligned_storage<sizeof(T), std::alignment_of<T>::value>::type t_;
void * pvt_ = &t_;

T* pT = new (&t_) T(args...);
void * pvT = pT;

тогда

  • память *pT точно перекрывает первый размер (T) байтов памяти t_, так что pvT == pvt_
  • pvt_ == static_cast<void*>(&t_)
  • static_cast<T*>(pvT) == pT
  • Вместе это дает static_cast<T*>(static_cast<void*>(&t_)) == pT
person JoergB    schedule 28.01.2013
comment
Что такое АБИКТ? - person Trevor Hickey; 10.09.2016
comment
Если к объекту только обращаются через указатели, полученные через размещение new, я не вижу причин, по которым компилятор должен заботиться о объявленном типе объекта, поскольку никакие обращения с использованием этого типа не могут быть псевдонимом доступа, сделанного с новым типом. Если объект записывается непосредственно как lvalue, а его адрес позже преобразуется в другой тип посредством размещения new, требуется, чтобы компилятор выполнял запись в исходное значение, а не переупорядочивал прошлые записи, поскольку новый тип в некоторых случаях может препятствовать оптимизации. Я думаю, что преимущества гарантированного поведения в этом случае перевешивают... - person supercat; 12.09.2016
comment
... небольшое потенциальное препятствие для оптимизации, но я не уверен, что авторы компиляторов разделят это мнение. Однако большая часть ценности директив выравнивания связана с возможностью хранения статической или автоматической продолжительности, которая может использоваться для хранения вещей известного максимального размера, но произвольных типов. - person supercat; 12.09.2016
comment
Насколько я могу судить, ABICT? - person Jeff Walden; 01.02.2017