Bagaimana seharusnya seseorang menggunakan std :: opsional?

134

Saya membaca dokumentasi std::experimental::optionaldan saya punya ide bagus tentang apa fungsinya, tetapi saya tidak mengerti kapan saya harus menggunakannya atau bagaimana saya harus menggunakannya. Situs ini belum berisi contoh apa pun yang membuat saya lebih sulit memahami konsep sebenarnya dari objek ini. Kapan std::optionalpilihan yang baik untuk digunakan, dan bagaimana cara mengompensasi apa yang tidak ditemukan dalam Standar sebelumnya (C ++ 11).

0x499602D2
sumber
19
Dokumen boost.optional mungkin memberi sedikit cahaya.
juanchopanza
Sepertinya std :: unique_ptr umumnya dapat melayani kasus penggunaan yang sama. Saya kira jika Anda memiliki hal yang bertentangan dengan yang baru, maka opsional mungkin lebih disukai, tetapi bagi saya tampaknya (pengembang | aplikasi) dengan pandangan seperti itu pada minoritas kecil ... AFAICT, opsional BUKAN ide bagus. Paling tidak, kebanyakan dari kita bisa hidup nyaman tanpanya. Secara pribadi, saya akan lebih nyaman di dunia di mana saya tidak harus memilih antara unique_ptr vs opsional. Sebut saya gila, tetapi Zen dari Python benar: biarkan ada satu cara yang tepat untuk melakukan sesuatu!
allyourcode
19
Kami tidak selalu ingin mengalokasikan sesuatu pada heap yang harus dihapus, jadi tidak ada unique_ptr bukan pengganti untuk opsional.
Krum
5
@allyourcode No pointer adalah pengganti optional. Bayangkan Anda menginginkan optional<int>atau bahkan <char>. Apakah Anda benar-benar berpikir bahwa "Zen" diperlukan untuk mengalokasikan, dereferensi, dan kemudian menghapus secara dinamis - sesuatu yang sebaliknya tidak memerlukan alokasi apa pun dan pas di tumpukan, dan mungkin bahkan dalam register?
underscore_d
1
Perbedaan lainnya, yang dihasilkan dari alokasi stack vs heap dari nilai yang terkandung, adalah bahwa argumen template tidak bisa menjadi tipe yang tidak lengkap dalam kasus std::optional(sementara bisa untuk std::unique_ptr). Lebih tepatnya, standar mensyaratkan bahwa T [...] harus memenuhi persyaratan Destructible .
dan_din_pantelimon

Jawaban:

173

Contoh paling sederhana yang dapat saya pikirkan:

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

Hal yang sama mungkin dapat diselesaikan dengan argumen referensi sebagai gantinya (seperti dalam tanda tangan berikut), tetapi menggunakan std::optionalmembuat tanda tangan dan penggunaan lebih baik.

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

Cara lain yang bisa dilakukan ini sangat buruk :

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

Ini membutuhkan alokasi memori yang dinamis, mengkhawatirkan kepemilikan, dll. - selalu lebih suka salah satu dari dua tanda tangan lainnya di atas.


Contoh lain:

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

Ini lebih disukai daripada memiliki sesuatu seperti std::unique_ptr<std::string>untuk setiap nomor telepon! std::optionalmemberi Anda lokalitas data, yang sangat bagus untuk kinerja.


Contoh lain:

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

Jika pencarian tidak memiliki kunci tertentu di dalamnya, maka kita dapat dengan mudah mengembalikan "tidak ada nilai."

Saya bisa menggunakannya seperti ini:

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

Contoh lain:

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

Ini jauh lebih masuk akal daripada, katakanlah, memiliki empat fungsi kelebihan yang mengambil setiap kemungkinan kombinasi max_count(atau tidak) dan min_match_score(atau tidak)!

Hal ini juga menghilangkan yang terkutuk "Lulus -1untuk max_countjika Anda tidak ingin batas" atau "Lulus std::numeric_limits<double>::min()untuk min_match_scorejika Anda tidak ingin skor minimal"!


Contoh lain:

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

Jika string kueri tidak ada s, saya ingin "tidak int" - bukan nilai khusus apa pun yang diputuskan seseorang untuk digunakan untuk tujuan ini (-1?).


Untuk contoh tambahan, Anda dapat melihat boost::optional dokumentasi . boost::optionaldan std::optionalpada dasarnya akan identik dalam hal perilaku dan penggunaan.

Perisai Timothy
sumber
13
@gnzlbg std::optional<T>hanya a Tdan a bool. Implementasi fungsi anggota sangat sederhana. Kinerja seharusnya tidak benar-benar menjadi perhatian ketika menggunakannya - ada kalanya sesuatu bersifat opsional, dalam hal ini sering kali merupakan alat yang tepat untuk pekerjaan itu.
Timothy Shields
8
@TimothyShields std::optional<T>jauh lebih kompleks dari itu. Ini menggunakan penempatan newdan lebih banyak hal lainnya dengan penjajaran dan ukuran yang tepat untuk membuatnya menjadi tipe literal (yaitu digunakan dengan constexpr) di antara hal-hal lainnya. Naif Tdan boolpendekatan akan gagal dengan cepat.
Rapptz
15
@Rapptz Baris 256: union storage_t { unsigned char dummy_; T value_; ... }Baris 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Bagaimana itu bukan "a Tdan a bool"? Saya sepenuhnya setuju bahwa penerapannya sangat rumit dan nontrivial, tetapi secara konsep dan konkret tipenya adalah a Tdan a bool. "Naif Tdan boolpendekatannya akan gagal dengan cepat." Bagaimana Anda bisa membuat pernyataan ini ketika melihat kode?
Timothy Shields
12
@ Rettz masih menyimpan bool dan ruang untuk int. Serikat pekerja hanya ada di sana untuk membuat opsi tidak membangun T jika sebenarnya tidak diinginkan. Itu masih struct{bool,maybe_t<T>}serikat hanya ada untuk tidak melakukan struct{bool,T}yang akan membangun T dalam semua kasus.
PeterT
12
@allyourcode Pertanyaan yang sangat bagus. Keduanya std::unique_ptr<T>dan std::optional<T>dalam beberapa hal memenuhi peran "opsional T". Saya akan menggambarkan perbedaan di antara mereka sebagai "detail implementasi": alokasi tambahan, manajemen memori, lokalitas data, biaya pemindahan, dll. Saya tidak akan pernah memiliki std::unique_ptr<int> try_parse_int(std::string s);contoh, karena itu akan menyebabkan alokasi untuk setiap panggilan meskipun tidak ada alasan untuk . Saya tidak akan pernah memiliki kelas dengan std::unique_ptr<double> limit;- mengapa alokasi dan kehilangan lokalitas data?
Timothy Shields
35

Contoh dikutip dari Makalah yang diadopsi baru: N3672, std :: opsional :

 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
sumber
13
Karena Anda dapat meneruskan informasi tentang apakah Anda mendapat inthierarki panggilan naik atau turun, alih-alih memberikan nilai "hantu" "yang dianggap" memiliki makna "kesalahan".
Luis Machuca
1
@ Wiz Ini sebenarnya adalah contoh yang bagus. Ini (A) memungkinkan str2int()penerapan konversi seperti yang diinginkannya, (B) terlepas dari metode untuk memperolehnya string s, dan (C) menyampaikan makna penuh melalui optional<int>alih - alih cara angka-angka, boolreferensi, atau alokasi dinamis berdasarkan alokasi lakukanlah.
underscore_d
10

tapi saya tidak mengerti kapan saya harus menggunakannya atau bagaimana saya harus menggunakannya.

Pertimbangkan ketika Anda menulis API dan Anda ingin menyatakan bahwa nilai "tidak memiliki pengembalian" bukanlah kesalahan. Misalnya, Anda perlu membaca data dari soket, dan ketika blok data selesai, Anda menguraikannya dan mengembalikannya:

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

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

Jika data yang ditambahkan menyelesaikan blok yang dapat diuraikan, Anda dapat memprosesnya; jika tidak, terus membaca dan menambahkan data:

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

Edit: tentang sisa pertanyaan Anda:

When std :: opsional merupakan pilihan yang baik untuk digunakan

  • Ketika Anda menghitung nilai dan perlu mengembalikannya, itu membuat semantik yang lebih baik untuk kembali dengan nilai daripada mengambil referensi ke nilai output (yang mungkin tidak dihasilkan).

  • Bila Anda ingin memastikan bahwa kode klien memiliki untuk memeriksa nilai output (siapa pun menulis kode klien mungkin tidak memeriksa kesalahan - jika Anda mencoba untuk menggunakan pointer un-diinisialisasi Anda mendapatkan core dump, jika Anda mencoba untuk menggunakan un- menginisialisasi std :: opsional, Anda mendapatkan pengecualian tangkapan-mampu).

[...] dan bagaimana cara mengompensasi apa yang tidak ditemukan dalam Standar sebelumnya (C ++ 11).

Sebelum ke C ++ 11, Anda harus menggunakan antarmuka yang berbeda untuk "fungsi yang mungkin tidak mengembalikan nilai" - baik kembali dengan pointer dan memeriksa NULL, atau menerima parameter output dan mengembalikan kode kesalahan / hasil untuk "tidak tersedia ".

Keduanya memaksakan upaya dan perhatian ekstra dari implementer klien untuk melakukannya dengan benar dan keduanya merupakan sumber kebingungan (yang pertama mendorong implementer klien untuk memikirkan operasi sebagai alokasi dan membutuhkan kode klien untuk menerapkan logika penanganan pointer dan yang kedua memungkinkan kode klien untuk melarikan diri dengan menggunakan nilai yang tidak valid / tidak diinisialisasi).

std::optional dengan baik menangani masalah yang timbul dengan solusi sebelumnya.

utnapistim
sumber
Saya tahu mereka pada dasarnya sama, tetapi mengapa Anda menggunakan boost::bukan std::?
0x499602D2
4
Terima kasih - saya memperbaikinya (saya gunakan boost::optionalkarena setelah sekitar dua tahun menggunakannya, itu dikodekan ke dalam korteks pre-frontal saya).
utnapistim
1
Ini mengejutkan saya sebagai contoh yang buruk, karena jika beberapa blok selesai hanya satu yang dapat dikembalikan dan sisanya dibuang. Fungsi ini seharusnya mengembalikan koleksi blok yang mungkin kosong.
Ben Voigt
Kamu benar; itu adalah contoh yang buruk; Saya telah memodifikasi contoh saya untuk "parse first block if available".
utnapistim
4

Saya sering menggunakan opsional untuk mewakili data opsional yang diambil dari file konfigurasi, yaitu untuk mengatakan di mana data itu (seperti dengan elemen yang diharapkan, namun tidak perlu, dalam dokumen XML) disediakan secara opsional, sehingga saya dapat secara eksplisit dan jelas menunjukkan jika data sebenarnya ada dalam dokumen XML. Terutama ketika data dapat memiliki status "tidak disetel", versus status "kosong" dan "set" (logika fuzzy). Dengan opsional, set dan tidak set jelas, juga kosong akan jelas dengan nilai 0 atau nol.

Ini dapat menunjukkan bagaimana nilai "tidak disetel" tidak setara dengan "kosong". Dalam konsep, penunjuk ke int (int * p) dapat menunjukkan ini, di mana null (p == 0) tidak disetel, nilai 0 (* p == 0) disetel dan kosongkan, dan nilai lainnya (* p <> 0) diatur ke nilai.

Sebagai contoh praktis, saya memiliki sepotong geometri yang ditarik dari dokumen XML yang memiliki nilai yang disebut flag render, di mana geometri dapat menimpa flag render (set), menonaktifkan flag render (set ke 0), atau tidak mempengaruhi bendera render (tidak disetel), opsional akan menjadi cara yang jelas untuk mewakili ini.

Jelas pointer ke int, dalam contoh ini, dapat mencapai tujuan, atau lebih baik, share pointer karena dapat menawarkan implementasi yang lebih bersih, namun, saya berpendapat ini tentang kejelasan kode dalam kasus ini. Apakah null selalu "tidak disetel"? Dengan sebuah pointer, itu tidak jelas, karena nol secara harfiah berarti tidak dialokasikan atau dibuat, meskipun bisa , namun tidak selalu berarti "tidak disetel". Perlu menunjukkan bahwa pointer harus dilepaskan, dan dalam praktik yang baik diatur ke 0, namun, seperti dengan pointer bersama, opsional tidak memerlukan pembersihan eksplisit, jadi tidak ada masalah mencampur pembersihan dengan opsional belum diatur.

Saya percaya ini tentang kejelasan kode. Kejelasan mengurangi biaya pemeliharaan kode, dan pengembangan. Pemahaman yang jelas tentang niat kode sangat berharga.

Penggunaan pointer untuk mewakili ini akan membutuhkan konsep pointer yang berlebihan. Untuk menyatakan "null" sebagai "tidak disetel", biasanya Anda mungkin melihat satu atau lebih komentar melalui kode untuk menjelaskan maksud ini. Itu bukan solusi yang buruk dan bukan opsional, namun, saya selalu memilih untuk implementasi implisit daripada komentar eksplisit, karena komentar tidak dapat ditegakkan (seperti dengan kompilasi). Contoh item implisit ini untuk pengembangan (artikel-artikel dalam pengembangan yang disediakan murni untuk menegakkan niat) termasuk berbagai cor gaya C ++, "const" (terutama pada fungsi anggota), dan tipe "bool", untuk beberapa nama. Mungkin Anda tidak benar-benar membutuhkan fitur kode ini, asalkan semua orang mematuhi niat atau komentar.

Kit10
sumber