Як слід використовувати std :: необов’язково?


133

Я читаю документацію std::experimental::optionalі маю гарне уявлення про те, що вона робить, але не розумію, коли я повинен її використовувати або як мені її використовувати. Цей веб-сайт не містить жодних прикладів, які ще важче зрозуміють справжню концепцію цього об'єкта. Коли це std::optionalвдалий вибір для використання та як це компенсувати те, що не було знайдено в попередньому Стандарті (C ++ 11).


19
Документи boost.optional можуть пролити трохи світла.
juanchopanza

Схоже, std :: unique_ptr, як правило, може обслуговувати ті ж випадки використання. Я думаю, якщо у вас є річ проти нового, то необов'язково може бути кращим, але мені здається, що (розробники | додатки) з таким поглядом на нове є невеликою меншиною ... AFAICT, необов'язково НЕ чудова ідея. Принаймні, більшість із нас могли б комфортно жити без цього. Особисто мені буде зручніше в світі, де мені не доведеться вибирати між унікальним_птром та необов’язковим. Назвіть мене божевільним, але Дзен Пітона є правильним: нехай буде один правильний спосіб щось зробити!
allyourcode

19
Ми не завжди хочемо виділяти щось на купі, яке потрібно видалити, тому жоден унікальний_птр не є заміною необов'язкового.
Крум

5
@allyourcode Жоден вказівник не замінює optional. Уявіть, що ви хочете optional<int>або навіть <char>. Ви дійсно думаєте, що потрібно "дзен" вимагати динамічного розподілу, дереференції та видалення - те, що в іншому випадку не може вимагати будь-якого розподілу та компактного розміщення на стеку, а можливо, навіть у реєстрі?
підкреслити

1
Ще одна відмінність, що виникає без сумніву при розподілі вмісту, що міститься у стеку проти купи, полягає в тому, що аргумент шаблону не може бути неповним типом у випадку std::optional(хоча це може бути для std::unique_ptr). Точніше, стандарт вимагає, щоб T [...] відповідав вимогам, що руйнуються .
dan_din_pantelimon

Відповіді:


172

Найпростіший приклад, про який я можу придумати:

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;
};

Це надзвичайно бажано, а не мати щось на зразок 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якщо ви не хочете межі» або «Passstd::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>- це просто Tа bool. Реалізація функції-члена надзвичайно проста. Продуктивність насправді не повинна викликати занепокоєння при її використанні - бувають випадки, коли щось є необов’язковим, і в цьому випадку це часто є правильним інструментом для роботи.
Тимофій Шилдс

8
@TimothyShields std::optional<T>набагато складніше за це. Він використовує розташування newта багато іншого з належним вирівнюванням та розміром, щоб зробити його буквальним типом (тобто використовувати з constexpr) серед інших речей. Наївність Tі boolпідхід провалилися б досить швидко.
Раппц

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. Об'єднання існує лише для того, щоб зробити необов'язковий не побудувати Т, якщо він насправді не потрібен. Це все-таки 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<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
         }
     }
}

13
Тому що ви можете передавати інформацію про те, чи отримали ви intчи ні ієрархію викликів вгору чи вниз, замість того, щоб передавати якесь значення "фантом", яке "передбачається", має значення "помилка".
Луїс Мачука

1
@Wiz Це насправді чудовий приклад. Він (A) дозволяє str2int()здійснити конверсію, як хоче, (B) незалежно від способу отримання string s, і (C) передає повне значення за допомогою optional<int>замість якогось дурного магічного числа, bool/ посилання або динамічного виділення, заснованого на способі роблю це.
підкреслюй_3

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 :: необов'язково - хороший вибір для використання

  • Коли ви обчислюєте значення та потребуєте його повернення, це робить кращою семантику повернення за значенням, ніж посилання на вихідне значення (яке може не бути згенеровано).

  • Якщо ви хочете , щоб переконатися , що клієнтський код має перевірити значення вихідного (хто пише код клієнта не може перевірити наявність помилок - якщо ви намагаєтеся використовувати не-инициализирован покажчик ви отримаєте дамп, якщо ви намагаєтеся використовувати, недосяжний ініціалізовано std :: необов'язково, ви отримуєте винятковий виняток).

[...] і як це компенсує те, що не було знайдено в попередньому Стандарті (C ++ 11).

Перед C ++ 11 вам довелося використовувати інший інтерфейс для "функцій, які не можуть повернути значення" - або повернутися вказівником і перевірити NULL, або прийняти вихідний параметр і повернути код помилки / результату для "недоступний ".

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

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


Я знаю, що вони в основному однакові, але чому ви використовуєте boost::замість них std::?
0x499602D2

4
Дякую - я виправив його (я використав, boost::optionalтому що приблизно через два роки його використання він важко кодується в моїй передній лобній корі).
utnapistim

1
Це вважає мене поганим прикладом, оскільки якби кілька блоків було завершено, можна було б повернути один, а решту відкинути. Замість цього функція повинна повертати можливо порожню колекцію блоків.
Бен Войгт

Ти маєш рацію; це був поганий приклад; Я змінив свій приклад для "розбору першого блоку, якщо він доступний".
utnapistim

4

Я часто використовую необов'язкові для представлення необов'язкових даних, витягнутих з файлів конфігурації, тобто де ці дані (наприклад, з очікуваним, але не потрібним елементом у документі XML) необов'язково надаються, так що я можу чітко і чітко показати, чи дані фактично були присутні в документі XML. Особливо, коли дані можуть мати стан "не встановлено" проти "порожнього" та "встановленого" стану (нечітка логіка). Якщо необов'язково встановити, а не встановити, це ясно, також порожнім було б зрозуміло значення 0 або null.

Це може показати, як значення "не встановлено" не еквівалентно "порожній". У понятті вказівник на int (int * p) може показати це, де нуль (p == 0) не встановлено, значення 0 (* p == 0) встановлено і порожнє, і будь-яке інше значення (* p <> 0) встановлюється значення.

Для практичного прикладу, у мене є фрагмент геометрії, витягнутий з документа XML, який мав значення, яке називається прапорами візуалізації, де геометрія може або замінити прапори візуалізації (набір), відключити прапори візуалізації (встановлено на 0), або просто не вплинути на прапори візуалізації (не встановлено), необов'язково був би зрозумілим способом представити це.

Зрозуміло, що вказівник на int у цьому прикладі може досягти мети, а ще краще - вказівника на спільний доступ, оскільки він може запропонувати більш чисту реалізацію, однак я можу стверджувати, що мова йде про ясність коду в цьому випадку. Чи нуль завжди є "не встановлено"? З вказівником це не зрозуміло, оскільки null буквально означає, що не виділяється або створюється, хоча це може , але не обов'язково означає "не встановлено". Варто зазначити, що вказівник повинен бути випущений, а в належній практиці встановлений на 0, однак, як і у спільному покажчику, необов'язковий не потребує явного очищення, тому немає проблем з перемішуванням очищення з необов'язковий параметр не був встановлений.

Я вважаю, що мова йде про ясність коду. Чіткість знижує витрати на обслуговування коду та його розробку. Чітке розуміння наміру коду неймовірно цінне.

Використання вказівника для представлення цього потребує перевантаження концепції вказівника. Щоб представити "null" як "not set", як правило, ви можете побачити один або кілька коментарів через код для пояснення цього наміру. Це не погане рішення замість необов’язкового, проте я завжди вибираю неявну реалізацію, а не явні коментарі, оскільки коментарі не підлягають виконанню (наприклад, шляхом компіляції). Приклади цих неявних елементів для розробки (тих статей у розробці, які надаються виключно для забезпечення наміру) включають різні титри стилів C ++, "const" (особливо щодо функцій членів) та тип "bool". Можливо, вам не потрібні ці функції коду, доки всі підкоряються намірам чи коментарям.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.