Как использовать std :: optional?

135

Я читаю документацию, std::experimental::optionalи у меня есть хорошее представление о том, что он делает, но я не понимаю, когда мне следует его использовать или как я должен его использовать. На сайте пока нет примеров, поэтому мне сложнее понять истинную концепцию этого объекта. Когда - std::optionalхороший выбор для использования и как он компенсирует то, что не было найдено в предыдущем стандарте (C ++ 11).

0x499602D2
источник
19
В boost.optional документы могли бы пролить некоторый свет.
juanchopanza
Похоже, что std :: unique_ptr обычно может служить тем же сценариям использования. Я предполагаю, что если вы возражаете против нового, то необязательный вариант может быть предпочтительнее, но мне кажется, что (разработчики | приложения) с таким взглядом на новые находятся в небольшом меньшинстве ... AFAICT, необязательный - НЕ отличная идея. По крайней мере, большинство из нас могло бы комфортно жить без этого. Лично мне было бы удобнее в мире, где мне не нужно выбирать между unique_ptr и optional. Можете назвать меня сумасшедшим, но Zen of Python прав: пусть будет один правильный способ что-то сделать!
allyourcode
19
Мы не всегда хотим выделять в куче что-то, что нужно удалить, поэтому no unique_ptr не заменяет optional.
Крам
5
@allyourcode Никакой указатель не может заменить optional. Представьте, что вы хотите optional<int>или даже <char>. Вы действительно думаете, что «дзен» требуется для динамического выделения, разыменования и последующего удаления - чего-то, что в противном случае не потребовало бы какого-либо выделения и компактно поместилось бы в стеке и, возможно, даже в регистре?
underscore_d
1
Еще одно различие, которое, несомненно, вытекает из распределения содержащегося значения между стеком и кучей, заключается в том, что аргумент шаблона не может быть неполным типом в случае std::optional(хотя это может быть для std::unique_ptr). Точнее, стандарт требует, чтобы T [...] удовлетворял требованиям Destructible .
dan_din_pantelimon

Ответы:

173

Самый простой пример, который я могу придумать:

std::optional<int> try_parse_int(std::string s)
{
    //try to parse an int from the given string,
    //and return "nothing" if you fail
}

То же самое может быть выполнено с помощью аргумента ссылки (как в следующей подписи), но использование std::optionalделает подпись и использование более удобными.

bool try_parse_int(std::string s, int& i);

Другой способ сделать это особенно плох :

int* try_parse_int(std::string s); //return nullptr if fail

Это требует динамического распределения памяти, беспокойства о праве собственности и т. Д. - всегда предпочитайте одну из двух других подписей выше.


Другой пример:

class Contact
{
    std::optional<std::string> home_phone;
    std::optional<std::string> work_phone;
    std::optional<std::string> mobile_phone;
};

Это крайне предпочтительно, чем иметь что-то вроде a std::unique_ptr<std::string>для каждого номера телефона! std::optionalдает вам локальность данных, которая отлично подходит для производительности.


Другой пример:

template<typename Key, typename Value>
class Lookup
{
    std::optional<Value> get(Key key);
};

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

Я могу использовать это так:

Lookup<std::string, std::string> location_lookup;
std::string location = location_lookup.get("waldo").value_or("unknown");

Другой пример:

std::vector<std::pair<std::string, double>> search(
    std::string query,
    std::optional<int> max_count,
    std::optional<double> min_match_score);

В этом гораздо больше смысла, чем, скажем, иметь четыре перегрузки функций, которые принимают все возможные комбинации max_count(или нет) и min_match_score(или нет)!

Он также устраняет в проклятое «Pass -1для , max_countесли вы не хотите предел» или «Pass std::numeric_limits<double>::min()для min_match_scoreесли вы не хотите , минимальный балл»!


Другой пример:

std::optional<int> find_in_string(std::string s, std::string query);

Если строки запроса нет s, я хочу «нет int» - не какое-то особое значение, которое кто-то решил использовать для этой цели (-1?).


Дополнительные примеры вы можете найти в boost::optional документации . boost::optionalи в std::optionalосновном будут идентичны с точки зрения поведения и использования.

Тимоти Шилдс
источник
13
@gnzlbg std::optional<T>- это просто a Tи a bool. Реализации функций-членов чрезвычайно просты. Производительность не должна быть проблемой при ее использовании - бывают случаи, когда что-то не является обязательным, и в этом случае это часто правильный инструмент для работы.
Тимоти Шилдс
8
@TimothyShields std::optional<T>намного сложнее. Он использует размещение newи многое другое с правильным выравниванием и размером, чтобы, constexprпомимо прочего, сделать его буквальным типом (то есть использовать с ). Наивность Tи boolподход быстро потерпели бы неудачу.
Rapptz
15
@Rapptz Строка 256: union storage_t { unsigned char dummy_; T value_; ... }Строка 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Как это не «а» Tи «а bool»? Я полностью согласен с тем, что реализация очень сложна и нетривиальна, но концептуально и конкретно типом является a Tи a bool. «Наивность Tи boolподход быстро потерпят неудачу». Как вы можете сделать это заявление, глядя на код?
Тимоти Шилдс
12
@Rapptz по-прежнему хранит bool и место для int. Объединение существует только для того, чтобы необязательный элемент не создавал T, если он на самом деле не нужен. По-прежнему существует struct{bool,maybe_t<T>}объединение, которого нельзя делать, struct{bool,T}что во всех случаях будет создавать букву T.
PeterT
12
@allyourcode Очень хороший вопрос. Оба std::unique_ptr<T>и std::optional<T>вовсе в некотором смысле выполняют роль «необязательного T». Я бы описал разницу между ними как «детали реализации»: дополнительные выделения, управление памятью, локальность данных, стоимость перемещения и т. Д. Я бы никогда не стал, std::unique_ptr<int> try_parse_int(std::string s);например, потому что это вызовет выделение для каждого вызова, даже если нет причин для , У меня никогда не было бы класса с std::unique_ptr<double> limit;- почему происходит распределение и потеря локальности данных?
Тимоти Шилдс
35

Пример цитируется из новой принятой статьи: N3672, std :: optional :

 optional<int> str2int(string);    // converts int to string if possible

int get_int_from_user()
{
     string s;

     for (;;) {
         cin >> s;
         optional<int> o = str2int(s); // 'o' may or may not contain an int
         if (o) {                      // does optional contain a value?
            return *o;                  // use the value
         }
     }
}
TAOCP
источник
13
Потому что вы можете передавать информацию о том, есть ли у вас intиерархия вызовов вверх или вниз, вместо того, чтобы передавать какое-то «фантомное» значение, которое, как «предполагается», имеет значение «ошибка».
Луис Мачука,
1
@Wiz Это отличный пример. Он (A) позволяет str2int()реализовать преобразование так, как он хочет, (B) независимо от метода получения string s, и (C) передает полное значение через optional<int>вместо глупого boolспособа на основе магического числа, / ссылки или динамического распределения Делать это.
underscore_d
10

но я не понимаю, когда мне следует его использовать или как я должен его использовать.

Подумайте, когда вы пишете API и хотите заявить, что «отсутствие возврата» не является ошибкой. Например, вам нужно прочитать данные из сокета, и когда блок данных будет завершен, вы проанализируете его и вернете:

class YourBlock { /* block header, format, whatever else */ };

std::optional<YourBlock> cache_and_get_block(
    some_socket_object& socket);

Если добавленные данные завершили анализируемый блок, вы можете его обработать; в противном случае продолжайте чтение и добавление данных:

void your_client_code(some_socket_object& socket)
{
    char raw_data[1024]; // max 1024 bytes of raw data (for example)
    while(socket.read(raw_data, 1024))
    {
        if(auto block = cache_and_get_block(raw_data))
        {
            // process *block here
            // then return or break
        }
        // else [ no error; just keep reading and appending ]
    }
}

Изменить: относительно остальных ваших вопросов:

Когда std :: optional - хороший выбор

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

  • Если вы хотите убедиться, что клиентский код должен проверять выходное значение (тот, кто пишет клиентский код, может не проверять наличие ошибок - если вы попытаетесь использовать неинициализированный указатель, вы получите дамп ядра; если вы попытаетесь использовать не- инициализированный std :: optional, вы получите перехватываемое исключение).

[...] и как он компенсирует то, что не было найдено в предыдущем стандарте (C ++ 11).

До C ++ 11 вам приходилось использовать другой интерфейс для «функций, которые могут не возвращать значение» - либо возвращать по указателю и проверять значение NULL, либо принимать выходной параметр и возвращать код ошибки / результата для «недоступно. ».

И то, и другое требует от разработчика клиента дополнительных усилий и внимания, чтобы сделать все правильно, и оба являются источником путаницы (первая заставляет разработчика клиента думать об операции как о выделении памяти и требует от клиентского кода реализации логики обработки указателя, а вторая позволяет клиентский код, чтобы избежать использования недопустимых / неинициализированных значений).

std::optional хорошо заботится о проблемах, возникающих с предыдущими решениями.

utnapistim
источник
Я знаю, что они в основном такие же, но почему вы используете boost::вместо std::?
0x499602D2
4
Спасибо - я исправил это (я использовал, boost::optionalпотому что примерно через два года использования он жестко закодирован в моей префронтальной коре).
утнапистим
1
Это кажется мне плохим примером, поскольку, если было завершено несколько блоков, можно было вернуть только один, а остальные отбросить. Вместо этого функция должна возвращать возможно пустой набор блоков.
Бен Фойгт
Ты прав; это был плохой пример; Я изменил свой пример на «проанализировать первый блок, если он доступен».
утнаписим
4

Я часто использую необязательные параметры для представления дополнительных данных, извлеченных из файлов конфигурации, то есть где эти данные (например, с ожидаемым, но не обязательным элементом в XML-документе) необязательно предоставляются, чтобы я мог явно и четко показать, если данные фактически присутствовали в XML-документе. Особенно, когда данные могут иметь состояние «не установлено», а не «пусто» и «установлено» (нечеткая логика). С необязательным, установленным и не установленным ясно, также пустым будет ясно со значением 0 или нулем.

Это может показать, что значение «не установлено» не эквивалентно «пусто». По идее, указатель на int (int * p) может показать это, где null (p == 0) не установлен, значение 0 (* p == 0) установлено и пусто, а любое другое значение (* p <> 0) устанавливается в значение.

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

Очевидно, что указатель на int в этом примере может достичь цели или, что лучше, указатель на общий ресурс, поскольку он может предложить более чистую реализацию, однако я бы сказал, что в этом случае речь идет о ясности кода. Всегда ли нуль "не установлен"? С указателем это не ясно, поскольку null буквально означает, что он не выделен или не создан, хотя может , но не обязательно может означать «не установлен». Стоит отметить, что указатель должен быть освобожден, и в хорошей практике установлен на 0, однако, как и в случае с общим указателем, необязательный параметр не требует явной очистки, поэтому не стоит смешивать очистку с необязательный не был установлен.

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

Использование указателя для представления этого потребовало бы перегрузки концепции указателя. Чтобы представить «null» как «not set», обычно вы можете увидеть один или несколько комментариев в коде, объясняющих это намерение. Это неплохое решение вместо необязательного, однако я всегда предпочитаю неявную реализацию, а не явные комментарии, поскольку комментарии не подлежат исполнению (например, путем компиляции). Примеры этих неявных элементов для разработки (те статьи в разработке, которые предоставляются исключительно для обеспечения намерения) включают различные приведения в стиле C ++, «const» (особенно для функций-членов) и тип «bool», и это лишь некоторые из них. Возможно, вам действительно не нужны эти функции кода, если все подчиняются намерениям или комментариям.

Kit10
источник