В чем преимущество std :: option` над std :: shared_ptr` и `std :: unique_ptr`?

Рассуждение std::optional является сделано, сказав что он может содержать или не содержать значение. Следовательно, это спасает нас от усилий по созданию, возможно, большого объекта, если он нам не нужен.

Например, фабрика здесь не будет создавать объект, если не выполнено какое-либо условие:

#include <string>
#include <iostream>
#include <optional>

std::optional<std::string> create(bool b)
{
if(b)
return "Godzilla"; //string is constructed
else
return {}; //no construction of the string required
}

Но тогда как это отличается от этого:

std::shared_ptr<std::string> create(bool b)
{
if(b)
return std::make_shared<std::string>("Godzilla"); //string is constructed
else
return nullptr; //no construction of the string required
}

Что мы выигрываем, добавляя std::optional над просто используя std::shared_ptr в общем?

5

Решение

Что мы выигрываем, добавляя std :: необязательный, вместо простого использования std :: shared_ptr в целом?

Допустим, вам нужно вернуть символ из функции с флагом «не значение». Если бы вы использовали std::shared_ptr за это у вас были бы огромные накладные расходы — char будет выделяться в динамической памяти, плюс std::shared_ptr будет поддерживать блок управления. В то время как станд :: опционально на другой стороне:

Если необязательный параметр содержит значение, оно гарантированно будет
выделяется как часть необязательного следа объекта, то есть без динамического
распределение памяти когда-либо происходит. Таким образом, необязательный объект моделирует
объект, а не указатель, хотя оператор * () и оператор -> ()
определены.

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

8

Другие решения

Указатель может быть или не быть NULL. Значит ли это что-то для вас, полностью зависит от вас. В некоторых сценариях nullptr это действительное значение, с которым вы имеете дело, а в других оно может использоваться как флаг для указания «нет значения, двигайтесь вперед».

С std::optional, есть явное определение «содержит значение» и «не содержит значение». Вы можете даже использовать тип указателя с опциональным!


Вот надуманный пример:

У меня есть класс с именем Personи я хочу лениво загрузить свои данные с диска. Мне нужно указать, были ли загружены некоторые данные или нет. Давайте используем указатель для этого:

class Person
{
mutable std::unique_ptr<std::string> name;
size_t uuid;
public:
Person(size_t _uuid) : uuid(_uuid){}
std::string GetName() const
{
if (!name)
name = PersonLoader::LoadName(uuid); // magic PersonLoader class knows how to read this person's name from disk
if (!name)
return "";
return *name;
}
};

Отлично, я могу использовать nullptr значение, чтобы указать, было ли имя загружено с диска.

Но что, если поле является необязательным? То есть, PersonLoader::LoadName() может вернуться nullptr для этого человека. Мы действительно хочу выйти на диск каждый раз кто-то запрашивает это имя?

Войти std::optional, Теперь мы можем отслеживать, если мы уже пытались загрузить имя а также если это имя пусто Без std::optional, решение этого было бы создать логическое значение isLoaded для имени, и действительно каждого необязательного поля. (Что если мы «просто инкапсулируем флаг в структуру»? Ну, тогда вы бы реализовали optional, но проделал еще хуже)

class Person
{
mutable std::optional<std::unique_ptr<std::string>> name;
size_t uuid;
public:
Person(size_t _uuid) : uuid(_uuid){}
std::string GetName() const
{
if (!name){ // need to load name from disk
name = PersonLoader::LoadName(uuid);
}
// else name's already been loaded, retrieve cached value
if (!name.value())
return "";
return *name.value();
}
};

Теперь нам не нужно каждый раз выходить на диск; std::optional позволяет нам проверить это. Я написал маленький пример в комментариях, демонстрирующих эту концепцию в меньшем масштабе

4

Важно отметить, что вы получаете известное, подхватываемое исключение вместо неопределенное поведение если вы попытаетесь получить доступ к value() от необязательного, когда его там нет. Таким образом, если что-то пойдет не так с optional у вас, вероятно, будет намного лучшее время для отладки, чем если бы вы использовали shared_ptr или т.п. (Обратите внимание, что * оператор разыменования на optional все еще дает UB в этом случае; с помощью value() это более безопасная альтернатива).

Кроме того, есть общее удобство таких методов, как value_or, которые позволяют легко указать значение по умолчанию. Для сравнения:

(t == nullptr) ? "default" : *t

с

t.value_or("default")

Последний и более читаемый и немного короче.

Наконец, хранилище для элемента в optional находится внутри объекта. Это означает, что optional требует больше памяти, чем указатель, если объект отсутствует; однако это также означает, что для помещения объекта в пустое динамическое размещение не требуется optional,

3

Необязательный тип значения, допускающий значение NULL.

shared_ptr является ссылочным подсчитанным типом ссылки, который может быть обнуляемым.

unique_ptr является ссылочным типом только для перемещения, который обнуляется.

Что у них общего, так это то, что они обнуляемы, что они могут отсутствовать.

Они отличаются тем, что два являются ссылочными типами, а другой — типом значения.

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

У ссылочных типов есть и другие преимущества. Перемещение ссылочного типа не требует перемещения исходных данных.

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

Рассуждая о shared_ptr это очень сложно. Если не использовать очень строгий набор средств управления для того, как он используется, становится практически невозможно узнать, какова продолжительность жизни данных. Рассуждая о unique_ptr это намного проще, так как вам просто нужно отследить, где он перемещается. Рассуждая о optionalВремя жизни тривиально (ну, так же, как то, что вы в него встроили).

Опциональный интерфейс был дополнен несколькими монадическими методами (например, .value_or), но эти методы часто можно легко добавить к любому допускаемому типу. Тем не менее, в настоящее время они существуют для optional и не для shared_ptr или же unique_ptr,

Еще одно большое преимущество опциональных функций заключается в том, что совершенно очевидно, что вы ожидаете, что они иногда обнуляются. В C ++ есть дурная привычка предполагать, что указатели и умные указатели не являются нулевыми, потому что они используются по причинам Другой чем быть обнуляемым.

Таким образом, код предполагает, что некоторые общие или уникальные ptr никогда не равны нулю. И это работает, как правило.

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

На практике я боюсь принимать unique_ptr<enum_flags> = nullptr в качестве аргумента, где я хочу сказать, что «эти флаги являются необязательными», потому что принудительное распределение кучи для вызывающей стороны кажется грубым. Но optional<enum_flags> не заставляет это звонить. Сама дешевизна optional заставляет меня хотеть использовать его во многих ситуациях, я бы нашел какую-то другую работу, если бы у меня был единственный обнуляемый тип — умный указатель.

Это устраняет большую часть соблазна для «значений флага», например int rows=-1;, optional<int> rows; имеет более ясное значение, и в отладке сообщит мне, когда я использую строки, не проверяя состояние «пусто».

Функции, которые могут разумно завершаться с ошибкой или не возвращать что-либо интересное, могут избежать значений флагов или выделения кучи и вернуть optional<R>, В качестве примера предположим, что у меня есть пул потоков, который можно удалить (скажем, пул потоков, который останавливает обработку, когда пользователь закрывает приложение).

Я мог бы вернуться std::future<R> из функции «задача очереди» и используйте исключения, чтобы указать, что пул потоков был отменен. Но это означает, что все использование пула потоков должно подвергаться аудиту на предмет потока кода исключения.

Вместо этого я мог бы вернуться std::future<optional<R>>и дать подсказку пользователю, что ему приходится иметь дело с «что произойдет, если процесс никогда не произошел» в своей логике.

Исключения «исходить» все еще могут возникать, но теперь они являются исключительными и не являются частью стандартных процедур отключения.

В некоторых из этих случаев expected<T,E> будет лучшим решением, когда оно будет в стандарте.

1

Что мы выигрываем, добавляя std :: необязательный, вместо простого использования std :: shared_ptr в целом?

@Slava упомянула преимущество отсутствия выделения памяти, но это дополнительное преимущество (хорошо, в некоторых случаях это может быть значительным преимуществом, но я хочу сказать, что это не главное).

Основным преимуществом (ИМХО) является более четкая семантика:

Возвращение указателя обычно означает (в современном C ++) «выделяет память», или «обрабатывает память», или «знает адрес в памяти того или иного».

Возврат необязательного значения означает, что «отсутствие результата этого вычисления не является ошибкой»: имя возвращаемого типа говорит вам о том, как был задан API (намерение API, а не реализация).

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

Наличие в стандарте необязательного типа гарантирует, что вы сможете писать более выразительные API.

0
По вопросам рекламы [email protected]