В последнее время я заинтересовался инициализацией. Одна из вещей, которые меня особенно интересуют, это std::Optional за его способность инициализировать экземпляр типа после того, как он был объявлен. Я попытался прочитать код внутри необязательного заголовка, но код слишком «напыщенный», чтобы я мог его понять.
Как std::Optional может задержать инициализацию объекта в стеке? Я предполагаю, что он просто резервирует sizeof(<whatever_type) количество байтов в стеке, а затем повторно интерпретирует эти байты для инициализации <whatever_bytes>. Но как это делается конкретно? Как это реализовано? Как я могу реализовать это сам?
Обновлено: чтобы уточнить, я знаю, что std::Optional в основном имеет логический член, чтобы отслеживать, инициализирован ли объект или нет, и другой член, который содержит данные.
Однако я не понимаю, как необязательно можно что-то инициализировать вручную.
Как он может уничтожить объект? Как он может восстановить новый после разрушения старого?
«Очевидный» способ представить std::optional<T>
— использовать указание, установлено ли значение вместе с union
, содержащим T
, т. е. что-то вроде этого:
template <typename T>
class optional {
bool isSet = false;
union { T value; };
public:
// ...
};
По умолчанию элементы в union
не инициализированы. Вместо этого вам нужно будет использовать размещение new
и ручное уничтожение, чтобы управлять временем жизни сущности в union
. Концептуально это похоже на использование массива байтов, но компилятор обрабатывает любые требования к выравниванию.
Вот программа с некоторыми показанными операциями:
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <cassert>
template <typename T>
class optional {
bool isSet = false;
union { T value; };
void destroy() { if (this->isSet) { this->isSet = true; this->value.~T(); } }
public:
optional() {}
~optional() { this->destroy(); }
optional& operator=(T&& v) {
this->destroy();
new(&this->value) T(std::move(v));
this->isSet = true;
return *this;
}
explicit operator bool() const { return this->isSet; }
T& operator*() { assert(this->isSet); return this->value; }
T const& operator*() const { assert(this->isSet); return this->value; }
};
int main()
{
optional<std::string> o, p;
o = "hello";
if (o) {
std::cout << "optional='" << *o << "'\n";
}
}
Аккуратный! Я думаю, что это первый вариант использования union
, который я хотел бы использовать в своем собственном коде.
Спасибо! Союз это! Однако у меня есть пара вопросов. Как я могу использовать «новое» для инициализации объектов в указанном месте в стеке? Я еще мало что знаю об управлении памятью, поэтому прошу прощения, если это глупый вопрос; единственный случай, когда я знаю ключевое слово «новое», - это динамическое размещение. Кроме того, безопасно ли реализовать необязательный таким образом? Я не часто использую союзы, потому что много слышал о том, что они небезопасны. Но я не знаю почему. Так что я посмотрю это. Тем не менее, есть ли что-нибудь особенно опасное в этой реализации необязательного класса, о которой вы знаете?
@Eljay: очевидно, std::variant<T...>
- это то же самое, только с дескриптором члена вместо логического флага. union
s, которые могут содержать интересные типы C++, были необходимы, чтобы разблокировать некоторые интересные функции. Есть несколько других случаев, когда отложенное построение стержней полезно.
@INEEDANSWERS: C++ union
— это объекты, о которых вам нужно заботиться явно. Вам нужно построить/уничтожить текущий установленный элемент, и доступ к другому, отличному от текущего набора, является неопределенным поведением (в случае optional
есть только один, поэтому вам просто нужно убедиться, что элемент установлен при его использовании). Есть еще много вещей, о которых вам нужно знать, и в этом смысле они менее безопасны, чем другие объекты, которые предназначены для простоты использования. Однако это инструменты для создания безопасных сущностей (например, std::optional
). Кстати, я расширил свой ответ, чтобы он содержал минимальную реализацию.
Спасибо за подробное обновление вашего ответа. Считаю свой вопрос ответом. Я не знал, что союзы могут задерживать инициализацию, и если бы я это сделал, я бы использовал его раньше. Еще один вопрос: когда вы говорите «Есть больше (...) простоты использования», все опасности, о которых вы говорите, связаны с использованием союзов, верно? Итак, как только я прочитаю о профсоюзах и узнаю, на что обращать внимание, мне пора идти, верно? Или вы говорите об ошибках, связанных с другими опасностями этой реализации?
@INEEDANSWERS: Ну, помимо того, что делает union
, вам нужно знать, чтобы написать правильный шаблон класса optional
. На самом деле, я только что заметил глупую ошибку, которую я сделал, и я исправлю: после destroy()
изменения value
код не очищается isSet
, но создание нового объекта может привести к тому, что value
останется неинициализированным, в то время как isSet
будет утверждать, что есть ошибка. Это не имеет ничего общего с union
s, но вам нужно быть бдительными, когда реализуете классы, явно вмешивающиеся в время жизни объекта (контейнеры обычно находятся в этом пространстве).
@DietmarKühl Ах да. Ну, конечно, на такие ошибки тоже важно обращать внимание, это правда. Спасибо за помощь. Теперь, когда я знаю, что эта реализация на самом деле является базовой опциональной, я начну изучать ключевое слово 'new', семантику перемещения (заметил там std::move) и объединения. Спасибо!
Это соответствующая часть необязательного заголовка в libc++: github.com/llvm-mirror/libcxx/blob/…