วิธีหนึ่งควรใช้ std :: ไม่จำเป็น?


133

ฉันอ่านเอกสารของstd::experimental::optionalและฉันมีความคิดที่ดีเกี่ยวกับสิ่งที่มันทำ แต่ฉันไม่เข้าใจเมื่อฉันควรใช้หรือวิธีที่ฉันควรใช้ ไซต์ยังไม่มีตัวอย่างใด ๆ ซึ่งทำให้ฉันเข้าใจแนวคิดที่แท้จริงของวัตถุนี้ได้ยากขึ้น เมื่อใดจึงstd::optionalเป็นทางเลือกที่ดีในการใช้งานและจะชดเชยสิ่งที่ไม่พบในมาตรฐานก่อนหน้านี้ (C ++ 11) ได้อย่างไร


19
เอกสาร boost.optionalอาจหลั่งน้ำตาแสงบาง
juanchopanza

ดูเหมือน std :: unique_ptr โดยทั่วไปสามารถให้บริการกรณีใช้เดียวกัน ฉันเดาว่าถ้าคุณมีสิ่งที่ต่อต้านใหม่ตัวเลือกอาจจะดีกว่า แต่ดูเหมือนว่าสำหรับฉัน (นักพัฒนา | แอป) ที่มีมุมมองแบบใหม่นั้นเป็นคนกลุ่มน้อย ... AFAICT ตัวเลือกไม่ใช่ความคิดที่ดี อย่างน้อยที่สุดพวกเราส่วนใหญ่ก็สามารถอยู่ได้โดยปราศจากมัน โดยส่วนตัวแล้วฉันจะรู้สึกสบายใจมากขึ้นในโลกที่ไม่ต้องเลือกระหว่าง unique_ptr กับตัวเลือก เรียกฉันว่าบ้า แต่ Zen of Python พูดถูก: ให้มีวิธีที่ถูกต้องในการทำอะไรสักอย่าง!
allyourcode

19
เราไม่ต้องการจัดสรรบางสิ่งบางอย่างบนฮีปที่จะต้องถูกลบเสมอดังนั้นจึงไม่มี unique_ptr แทนตัวเลือก
Krum

5
@allyourcode optionalไม่มีตัวชี้แทนสำหรับ ลองนึกภาพคุณต้องการหรือแม้กระทั่งoptional<int> <char>คุณคิดว่าเป็น "เซน" หรือไม่ที่จะต้องจัดสรรแบบไดนามิกยกเลิกการอ้างอิงและลบ - สิ่งที่อาจไม่จำเป็นต้องมีการจัดสรรและพอดีกับสแต็กและอาจอยู่ในการลงทะเบียนหรือไม่?
underscore_d

1
ความแตกต่างอีกประการหนึ่งทำให้ไม่ต้องสงสัยเลยว่าการจัดสรรสแต็ค vs heap ของค่าที่มีอยู่คืออาร์กิวเมนต์ของเทมเพลตไม่สามารถเป็นประเภทที่ไม่สมบูรณ์ในกรณีของ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(หรือไม่)!

นอกจากนี้ยังช่วยลดสาปแช่ง "ผ่าน-1สำหรับmax_countถ้าคุณไม่ต้องการขีด จำกัด" หรือ "ผ่าน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>เป็นเพียงและT boolการใช้งานฟังก์ชั่นสมาชิกนั้นง่ายมาก ประสิทธิภาพไม่ควรกังวลเมื่อใช้งาน - มีบางครั้งที่มีบางอย่างเป็นตัวเลือกซึ่งในกรณีนี้มักเป็นเครื่องมือที่ถูกต้องสำหรับงาน
Timothy Shields

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_; ... }นั่นไม่ใช่ "a Tและ a bool" อย่างไร ผมเห็นด้วยการดำเนินการเป็นเรื่องยุ่งยากมากและขี้ปะติ๋ว แต่แนวคิดและรูปธรรมชนิดเป็นและT bool"ความไร้เดียงสาTและboolแนวทางจะล้มเหลวอย่างรวดเร็ว" คุณจะสร้างคำแถลงนี้เมื่อดูรหัสได้อย่างไร
Timothy Shields

12
@apptz มันยังคงเก็บบูลและพื้นที่สำหรับ 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;- ทำไมการจัดสรรและสูญเสียตำแหน่งข้อมูล?
Timothy Shields

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ลำดับชั้นการโทรขึ้นหรือลงหรือไม่แทนที่จะส่งผ่านค่า "phantom" "สันนิษฐาน" เพื่อให้มีความหมาย "ข้อผิดพลาด"
Luis Machuca

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

หากข้อมูลต่อท้ายเสร็จสมบูรณ์แล้วบล็อก parsable คุณสามารถประมวลผลได้ มิฉะนั้นให้อ่านและต่อท้ายข้อมูล:

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

แก้ไข: เกี่ยวกับคำถามที่เหลือของคุณ:

เมื่อใดคือมาตรฐาน :: ตัวเลือกเป็นทางเลือกที่ดีที่จะใช้

  • เมื่อคุณคำนวณค่าและต้องการคืนค่ามันจะทำให้ซีแมนทิกส์ดีกว่าเพื่อส่งคืนตามค่ามากกว่าการอ้างอิงถึงค่าเอาต์พุต (ซึ่งอาจไม่ได้สร้างขึ้น)

  • เมื่อคุณต้องการเพื่อให้แน่ใจว่ารหัสลูกค้าที่มีการตรวจสอบค่าเอาท์พุท (ใครก็ตามที่เขียนรหัสลูกค้าอาจไม่ได้ตรวจสอบข้อผิดพลาด - ถ้าคุณพยายามที่จะใช้ตัวชี้ยกเลิกที่เริ่มคุณจะได้รับถ่ายโอนข้อมูลหลักถ้าคุณพยายามที่จะยกเลิกการใช้ initialized std :: เป็นทางเลือกคุณจะได้รับข้อยกเว้น catch-can)

[... ] และมันชดเชยสิ่งที่ไม่พบในมาตรฐานก่อนหน้า (C ++ 11) ได้อย่างไร

ก่อนหน้า C ++ 11 คุณต้องใช้อินเทอร์เฟซต่าง ๆ สำหรับ "ฟังก์ชั่นที่อาจไม่ส่งคืนค่า" - ส่งคืนโดยตัวชี้และตรวจสอบค่า NULL หรือยอมรับพารามิเตอร์เอาต์พุตและส่งคืนรหัสข้อผิดพลาด / ผลลัพธ์สำหรับ "ไม่พร้อมใช้งาน "

ทั้งสองกำหนดความพยายามและความสนใจเป็นพิเศษจากผู้ดำเนินการไคลเอนต์เพื่อทำให้ถูกต้องและทั้งคู่เป็นแหล่งของความสับสน (ครั้งแรกที่ผลักดันให้ผู้ดำเนินการไคลเอนต์คิดว่าการดำเนินการเป็นการจัดสรรและต้องใช้รหัสไคลเอนต์ รหัสลูกค้าที่จะใช้งานโดยใช้ค่าที่ไม่ถูกต้อง / ไม่ได้กำหนดค่าเริ่มต้น)

std::optional อย่างดูแลปัญหาที่เกิดขึ้นกับโซลูชั่นก่อนหน้านี้


ฉันรู้ว่าพวกเขากำลังพื้นเดียวกัน แต่ทำไมคุณใช้boost::แทนstd::?
0x499602D2

4
ขอบคุณ - ฉันแก้ไขมัน (ฉันใช้boost::optionalเพราะหลังจากนั้นประมาณสองปีในการใช้มันเป็นรหัสยากในเยื่อหุ้มสมองก่อนหน้าผากของฉัน)
utnapistim

1
สิ่งนี้ทำให้ฉันเป็นตัวอย่างที่ไม่ดีเนื่องจากหากมีหลายบล็อกที่เสร็จสมบูรณ์เพียงหนึ่งบล็อกเท่านั้นที่สามารถส่งคืนได้และส่วนที่เหลือจะถูกยกเลิก ฟังก์ชันควรส่งคืนกลุ่มของบล็อกที่อาจว่างเปล่าแทน
Ben Voigt

คุณถูก; มันเป็นตัวอย่างที่น่าสงสาร ฉันได้แก้ไขตัวอย่างสำหรับ "parse first block ถ้ามี"
utnapistim

4

ฉันมักจะใช้ตัวเลือกเพื่อแสดงข้อมูลทางเลือกที่ดึงมาจากไฟล์การกำหนดค่าซึ่งก็คือบอกว่าข้อมูลนั้น (เช่นที่คาดว่าจะมี แต่ไม่จำเป็นต้องมีองค์ประกอบภายในเอกสาร XML) เป็นทางเลือกดังนั้นฉันจึงสามารถแสดงอย่างชัดเจนและชัดเจนว่า ข้อมูลมีอยู่จริงในเอกสาร XML โดยเฉพาะอย่างยิ่งเมื่อข้อมูลสามารถมีสถานะ "ไม่ได้ตั้งค่า" เมื่อเทียบกับสถานะ "ว่างเปล่า" และ "ตั้งค่า" (ตรรกะคลุมเครือ) ด้วยตัวเลือกตั้งค่าและไม่ได้ตั้งค่าเป็นที่ว่างเปล่านอกจากนี้ยังจะล้างด้วยค่า 0 หรือ null

สิ่งนี้สามารถแสดงได้ว่าค่าของ "ไม่ได้ตั้งค่า" ไม่เท่ากับ "ว่าง" ในแนวคิดตัวชี้ไปยัง int (int * p) สามารถแสดงสิ่งนี้โดยที่ไม่ได้ตั้งค่า null (p == 0) ค่า 0 (* p == 0) จะถูกตั้งค่าและว่างเปล่าและค่าอื่น ๆ (* p <> 0) ตั้งเป็นค่า

สำหรับตัวอย่างที่ใช้งานได้จริงฉันได้ดึงชิ้นส่วนของรูปทรงเรขาคณิตจากเอกสาร XML ที่มีค่าที่เรียกว่าธงแสดงผลซึ่งรูปทรงเรขาคณิตสามารถแทนที่การตั้งค่าการแสดงผลธง (ชุด) ปิดการใช้งานธงการตั้งค่า (0) ส่งผลกระทบต่อการแสดงผลธง (ไม่ได้ตั้งค่า), ตัวเลือกจะเป็นวิธีที่ชัดเจนในการเป็นตัวแทนนี้

ตัวชี้ไปยัง int อย่างชัดเจนในตัวอย่างนี้สามารถบรรลุเป้าหมายได้หรือดีกว่าตัวชี้การแชร์เนื่องจากสามารถเสนอการใช้ที่สะอาดขึ้นได้อย่างไรก็ตามฉันจะโต้แย้งว่ามันเกี่ยวกับความชัดเจนของรหัสในกรณีนี้ null เป็น "ไม่ได้ตั้งค่า" เสมอหรือไม่ ด้วยตัวชี้มันไม่ชัดเจนเนื่องจาก null หมายถึงไม่จัดสรรหรือสร้างแม้ว่ามันจะทำได้แต่อาจไม่ได้หมายความว่า "ไม่ได้ตั้งค่า" เป็นค่าที่ชี้ให้เห็นว่าตัวชี้จะต้องได้รับการปล่อยตัวและในทางปฏิบัติที่ดีตั้งค่าเป็น 0 อย่างไรก็ตามเช่นเดียวกับตัวชี้ที่ใช้ร่วมกันตัวเลือกไม่จำเป็นต้องล้างข้อมูลอย่างชัดเจนดังนั้นจึงไม่ต้องกังวลกับการล้างข้อมูลด้วย ไม่ได้ตั้งค่าเผื่อเลือก

ฉันเชื่อว่ามันเกี่ยวกับความชัดเจนของรหัส ความชัดเจนลดค่าใช้จ่ายในการบำรุงรักษาโค้ดและการพัฒนา ความเข้าใจที่ชัดเจนเกี่ยวกับความตั้งใจของรหัสนั้นมีค่าอย่างเหลือเชื่อ

การใช้ตัวชี้เพื่อแทนค่านี้จะต้องใช้แนวคิดของตัวชี้มากไป ในการแสดง "null" เป็น "ไม่ได้ตั้งค่า" โดยทั่วไปคุณอาจเห็นความคิดเห็นหนึ่งรายการหรือมากกว่าผ่านโค้ดเพื่ออธิบายความตั้งใจนี้ นั่นไม่ใช่วิธีแก้ปัญหาที่ไม่ดีแทนที่จะเป็นทางเลือกอย่างไรก็ตามฉันมักเลือกใช้การนำไปใช้โดยนัยแทนที่จะแสดงความคิดเห็นอย่างชัดเจนเนื่องจากความคิดเห็นไม่สามารถบังคับใช้ได้ (เช่นโดยการรวบรวม) ตัวอย่างของรายการโดยนัยเหล่านี้สำหรับการพัฒนา (บทความที่อยู่ในการพัฒนาที่มีไว้เพื่อบังคับใช้ความตั้งใจเท่านั้น) รวมถึงการใช้งานสไตล์ C ++ ที่หลากหลาย, "const" (โดยเฉพาะในฟังก์ชั่นสมาชิก) และประเภท "บูล" คุณไม่ต้องการฟีเจอร์โค้ดเหล่านี้ตราบใดที่ทุกคนปฏิบัติตามความตั้งใจหรือความคิดเห็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.