Std::необязательный - создать пустое с помощью {} или std::nullopt?
Я думал, что инициализация std::optional
с помощью std::nullopt
будет такой же, как и конструкция по умолчанию.
Они описываются так же, как и в, как форма (1)
Однако и Clang, и GCC, похоже, по-разному относятся к этим функциям примера игрушек.
#include <optional>
struct Data {
char large_data[0x10000];
};
std::optional<Data> nullopt_init() {
return std::nullopt;
}
std::optional<Data> default_init() {
return {};
}
Компилятор, похоже, подразумевает, что использование std::nullopt
просто установит флаг "содержит",
nullopt_init():
mov BYTE PTR [rdi+65536], 0
mov rax, rdi
ret
В то время как конструкция по умолчанию будет иметь значение инициализации всего класса. Это функционально эквивалентно, но почти всегда дороже.
default_init():
sub rsp, 8
mov edx, 65537
xor esi, esi
call memset
add rsp, 8
ret
Это преднамеренное поведение? Когда одна форма должна быть предпочтительнее другой?
Ответы
Ответ 1
Для gcc ненужное обнуление с инициализацией по умолчанию
std::optional<Data> default_init() {
std::optional<Data> o;
return o;
}
это ошибка 86173 и должна быть исправлена в самом компиляторе. Используя тот же libstdc++, clang не выполняет здесь никаких настроек.
Теперь в вашем коде вы фактически инициализируете значение объекта (через list-initialization). Похоже, что реализации библиотеки std::optional
имеют 2 основных варианта: либо они делают конструктор по умолчанию тривиальным (libstdc++), что имеет некоторые преимущества, но вызывает нулевую инициализацию всего буфера; или они предоставляют конструктор по умолчанию (libc++), который инициализирует только то, что нужно (например, конструктор из std::nullopt
), но они теряют тривиальность. К сожалению, кажется невозможным иметь преимущества обоих. Я думаю, что я предпочитаю второй вариант. Тем временем, прагматично, использование конструктора из std::nullopt
, где он не усложняет код, кажется хорошей идеей.
Ответ 2
В этом случае {}
вызывает инициализацию значения. Если optional
конструктор по умолчанию не предоставлен пользователем (где "не предоставлен пользователем" означает грубо "неявно объявлен или явно задан по умолчанию в определении класса"), то происходит нулевая инициализация всего объекта.
Будет ли это сделано, зависит от деталей реализации этой конкретной реализации std::optional
. Похоже, что конструктор по умолчанию libstdc++ optional
не предоставлен пользователем, а libc++ -.
Ответ 3
Стандарт ничего не говорит о реализации этих двух конструкторов. По словам [опционально .ctor]:
constexpr optional() noexcept;
constexpr optional(nullopt_t) noexcept;
- Гарантирует:
*this
не содержит значения.
- Примечания: не содержится значение инициализируется. Для каждого типа объекта
T
эти конструкторы должны быть конструкторами constexpr
(9.1.5).
Он просто определяет сигнатуру этих двух конструкторов и их "Ensures" (иначе эффекты): после любой из этих конструкций optional
не содержит никакого значения. Других гарантий не дано.
То, является ли первый конструктор пользовательским, определяется реализацией (т.е. зависит от компилятора).
Если первый конструктор определяется пользователем, он, конечно, может быть реализован как установка флага contains
. Но не определяемый пользователем конструктор также совместим со стандартом (как реализовано в gcc), потому что это также инициализирует ноль флаг false
. Хотя это приводит к дорогостоящей инициализации нуля, оно не нарушает "гарантии", определенные стандартом.
Что касается использования в реальной жизни, хорошо, что вы погрузились в реализации, чтобы написать оптимальный код.
Как примечание, вероятно, стандарт должен определять сложность этих двух конструкторов (т.е. O(1)
или O(sizeof(T))
)
Ответ 4
Мотивационный пример
Когда я пишу:
std::optional<X*> opt{};
(*opt)->f();//expects error here, not UB or heap corruption
Я ожидаю, что дополнительный инициализируется и не содержит неинициализированной памяти. Кроме того, я не ожидал, что последствием будет повреждение кучи, поскольку я ожидаю, что все инициализировано нормально. Это сравнивается с семантикой указателя std::optional
:
X* ptr{};//ptr is now zero
ptr->f();//deterministic error here, not UB or heap corruption
Если бы я написал std::optional<X*>(std::nullopt)
, я бы надеялся на то же самое, но, по крайней мере, здесь это выглядит более неоднозначно.
Причина в неинициализированной памяти
Весьма вероятно, что такое поведение является преднамеренным.
(Я не являюсь частью какого-либо комитета, поэтому в конце я не могу сказать наверняка)
Это основная причина: пустая фигурная скобка init (zero-init) не должна приводить к неинициализированной памяти (хотя язык не предписывает это как правило) - как еще вы гарантируете отсутствие неинициализированной памяти в ваша программа?
Для этой задачи мы часто обращаемся к инструментам статического анализа: проверке ядра cpp, основанной на применении основных принципов cpp; в частности, есть несколько рекомендаций, касающихся именно этой проблемы. Если бы это было невозможно, наш статический анализ потерпел бы неудачу в этом, казалось бы, простом случае; или хуже, вводить в заблуждение. Напротив, контейнеры на основе кучи, естественно, не имеют такой же проблемы.
Неограниченный доступ
Помните, что доступ к std::optional
не проверен - это приводит к тому случаю, когда вы можете по ошибке получить доступ к этой унифицированной памяти.
Просто, чтобы продемонстрировать это, если бы это было не так, то это может быть кучей коррупции:
std::optional<X*> opt{};//lets assume brace-init does not zero-initialize the underlying object for a moment (in practice it does)
(*opt)->f();//<- possible heap corruption
Однако в текущей реализации это становится детерминированным (ошибка сегмента/нарушение доступа на основных платформах).
Тогда вы можете спросить, почему "специализированный" конструктор std::nullopt
не инициализирует память?
Я не совсем уверен, почему это не так. Хотя я думаю, что это не будет проблемой, если будет. В этом случае, в отличие от брейс-инициала, он не имеет таких же ожиданий. Тонко, теперь у вас есть выбор.
Для тех, кто заинтересован, MSVC делает то же самое.