Làm thế nào nên sử dụng std :: tùy chọn?


133

Tôi đang đọc tài liệu về std::experimental::optionalvà tôi có một ý tưởng tốt về những gì nó làm, nhưng tôi không hiểu khi nào tôi nên sử dụng nó hay tôi nên sử dụng nó như thế nào. Trang web không chứa bất kỳ ví dụ nào cho đến khi tôi khó nắm bắt khái niệm thực sự của đối tượng này. Khi nào là std::optionalmột lựa chọn tốt để sử dụng và làm thế nào để bù đắp cho những gì không tìm thấy trong Tiêu chuẩn trước đó (C ++ 11).


19
Các tài liệu boost.optional có thể làm sáng tỏ.
juanchopanza

Có vẻ như std :: unique_ptr thường có thể phục vụ các trường hợp sử dụng tương tự. Tôi đoán rằng nếu bạn có một điều chống lại cái mới, thì tùy chọn có thể thích hợp hơn, nhưng dường như với tôi rằng (nhà phát triển | ứng dụng) với quan điểm đó về cái mới là trong một thiểu số nhỏ ... AFAICT, tùy chọn KHÔNG phải là một ý tưởng tuyệt vời. Ít nhất, hầu hết chúng ta có thể thoải mái sống mà không cần nó. Cá nhân, tôi sẽ thoải mái hơn trong một thế giới mà tôi không phải lựa chọn giữa unique_ptr so với tùy chọn. Gọi tôi là điên, nhưng Zen của Python là đúng: hãy có một cách đúng để làm một cái gì đó!
mã allyour

19
Chúng tôi không phải lúc nào cũng muốn phân bổ thứ gì đó trong heap phải xóa, vì vậy không có unique_ptr nào không thay thế cho tùy chọn.
Krum

5
@allyourcode Không có con trỏ nào thay thế được optional. Hãy tưởng tượng bạn muốn một optional<int>hoặc thậm chí <char>. Bạn có thực sự nghĩ rằng "Zen" phải được yêu cầu phân bổ động, tính trung thực và sau đó xóa - thứ gì đó không thể yêu cầu bất kỳ phân bổ nào và nằm gọn trong ngăn xếp, và thậm chí có thể trong một thanh ghi?
gạch dưới

1
Một sự khác biệt khác, dẫn đến sự nghi ngờ từ phân bổ stack so với heap của giá trị được chứa, đó là đối số khuôn mẫu có thể là một kiểu không hoàn chỉnh trong trường hợp std::optional(trong khi nó có thể được dùng std::unique_ptr). Chính xác hơn, tiêu chuẩn yêu cầu T [...] phải đáp ứng các yêu cầu của Dest phá hủy .
dan_din_pantelimon

Câu trả lời:


172

Ví dụ đơn giản nhất tôi có thể nghĩ ra:

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

Thay vào đó, điều tương tự có thể được thực hiện bằng một đối số tham chiếu (như trong chữ ký sau), nhưng việc sử dụng std::optionallàm cho chữ ký và cách sử dụng đẹp hơn.

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

Một cách khác mà điều này có thể được thực hiện là đặc biệt xấu :

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

Điều này đòi hỏi phân bổ bộ nhớ động, lo lắng về quyền sở hữu, v.v. - luôn thích một trong hai chữ ký khác ở trên.


Một vi dụ khac:

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

Điều này là cực kỳ thích hợp để thay vì có một cái gì đó giống như std::unique_ptr<std::string>cho mỗi số điện thoại! std::optionalcung cấp cho bạn dữ liệu địa phương, đó là tuyệt vời cho hiệu suất.


Một vi dụ khac:

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

Nếu tra cứu không có khóa nhất định, thì chúng ta có thể trả về "không có giá trị".

Tôi có thể sử dụng nó như thế này:

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

Một vi dụ khac:

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

Điều này có ý nghĩa hơn nhiều so với, có bốn chức năng quá tải mà có mọi sự kết hợp có thể có max_count(hoặc không) và min_match_score(hoặc không)!

Nó cũng loại bỏ các nguyền rủa "Pass -1cho max_countnếu bạn không muốn có một giới hạn" hoặc "Pass std::numeric_limits<double>::min()cho min_match_scorenếu bạn không muốn có một số điểm tối thiểu"!


Một vi dụ khac:

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

Nếu chuỗi truy vấn không tham gia s, tôi muốn "không int" - không phải bất kỳ giá trị đặc biệt nào mà ai đó quyết định sử dụng cho mục đích này (-1?).


Để biết thêm ví dụ, bạn có thể xem boost::optional tài liệu . boost::optionalstd::optionalvề cơ bản sẽ giống hệt nhau về hành vi và cách sử dụng.


13
@gnzlbg std::optional<T>chỉ là a Tvà a bool. Việc thực hiện chức năng thành viên là vô cùng đơn giản. Hiệu suất không thực sự là một mối quan tâm khi sử dụng nó - có những lúc một cái gì đó là tùy chọn, trong trường hợp này thường là công cụ chính xác cho công việc.
Timothy Shields

8
@TimothyShields std::optional<T>phức tạp hơn thế rất nhiều. Nó sử dụng vị trí newvà nhiều thứ khác với sự liên kết và kích thước phù hợp để biến nó thành một kiểu chữ (nghĩa là sử dụng với constexpr) trong số những thứ khác. Sự ngây thơ Tboolcách tiếp cận sẽ thất bại khá nhanh.
Rapptz

15
@Rapptz Dòng 256: union storage_t { unsigned char dummy_; T value_; ... }Dòng 289: struct optional_base { bool init_; storage_t<T> storage_; ... }Làm thế nào mà không phải là "a Tvà a bool"? Tôi hoàn toàn đồng ý việc thực hiện là rất khó khăn và không cần thiết, nhưng về mặt khái niệm và cụ thể thì loại này là a Tvà a bool. "Sự ngây thơ Tboolcách tiếp cận sẽ thất bại khá nhanh." Làm thế nào bạn có thể đưa ra tuyên bố này khi nhìn vào mã?
Timothy Shields

12
@Rapptz nó vẫn lưu trữ một bool và không gian cho một int. Liên minh chỉ ở đó để làm cho tùy chọn không xây dựng T nếu nó không thực sự muốn. Vẫn struct{bool,maybe_t<T>}là liên minh chỉ ở đó để không làm điều struct{bool,T}đó sẽ tạo ra một T trong mọi trường hợp.
PeterT

12
@allyourcode Câu hỏi rất hay. Cả hai std::unique_ptr<T>std::optional<T>làm trong một số ý nghĩa hoàn thành vai trò của "một tùy chọn T." Tôi sẽ mô tả sự khác biệt giữa chúng là "chi tiết triển khai": phân bổ thêm, quản lý bộ nhớ, địa phương dữ liệu, chi phí di chuyển, v.v. Tôi sẽ không bao giờ có std::unique_ptr<int> try_parse_int(std::string s);, vì nó sẽ gây ra sự phân bổ cho mỗi cuộc gọi mặc dù không có lý do gì để . Tôi sẽ không bao giờ có một lớp với một std::unique_ptr<double> limit;- tại sao phân bổ và mất địa phương dữ liệu?
Timothy Shields

35

Một ví dụ được trích dẫn từ giấy mới được thông qua: N3672, std :: tùy chọn :

 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
Bởi vì bạn có thể truyền thông tin về việc bạn có intphân cấp cuộc gọi lên hay xuống, thay vì chuyển qua một số giá trị "ảo" "giả định" có nghĩa là "lỗi".
Luis Machuca

1
@Wiz Đây thực sự là một ví dụ tuyệt vời. Nó (A) cho phép str2int()thực hiện chuyển đổi theo cách mà nó muốn, (B) bất kể phương thức lấy string svà (C) truyền đạt ý nghĩa đầy đủ thông qua optional<int>thay vì một số cách ma thuật ngu ngốc, booltham chiếu hoặc phân bổ động làm việc đó.
gạch dưới

10

nhưng tôi không hiểu khi nào tôi nên sử dụng nó hay tôi nên sử dụng nó như thế nào.

Hãy xem xét khi bạn đang viết API và bạn muốn thể hiện rằng giá trị "không có lợi nhuận" không phải là một lỗi. Ví dụ: bạn cần đọc dữ liệu từ một ổ cắm và khi khối dữ liệu hoàn tất, bạn phân tích cú pháp và trả lại:

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

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

Nếu dữ liệu được nối thêm hoàn thành một khối có thể phân tích cú pháp, bạn có thể xử lý nó; mặt khác, tiếp tục đọc và nối thêm dữ liệu:

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

Chỉnh sửa: liên quan đến phần còn lại của câu hỏi của bạn:

Khi nào std :: tùy chọn là một lựa chọn tốt để sử dụng

  • Khi bạn tính toán một giá trị và cần trả về nó, nó sẽ làm cho ngữ nghĩa tốt hơn trả về theo giá trị hơn là tham chiếu đến một giá trị đầu ra (có thể không được tạo ra).

  • Khi bạn muốn đảm bảo rằng mã khách hàng để kiểm tra giá trị đầu ra (ai viết mã khách hàng có thể không kiểm tra lỗi - nếu bạn cố gắng sử dụng một con trỏ un-khởi tạo bạn sẽ có được một bãi chứa lõi, nếu bạn cố gắng sử dụng một un- khởi tạo std :: tùy chọn, bạn nhận được một ngoại lệ có thể bắt được).

[...] Và làm thế nào để nó bù đắp cho những gì không tìm thấy trong Tiêu chuẩn trước đó (C ++ 11).

Trước C ++ 11, bạn phải sử dụng một giao diện khác cho "các hàm không thể trả về giá trị" - trả về bằng con trỏ và kiểm tra NULL hoặc chấp nhận tham số đầu ra và trả về mã lỗi / kết quả cho "không khả dụng ".

Cả hai đều áp đặt thêm nỗ lực và sự chú ý từ người triển khai ứng dụng khách để làm cho đúng và cả hai đều là một sự nhầm lẫn (việc đầu tiên khiến người thực hiện khách hàng nghĩ về một hoạt động như một sự phân bổ và yêu cầu mã máy khách thực hiện logic xử lý con trỏ và cho phép thứ hai mã khách hàng để thoát khỏi việc sử dụng các giá trị không hợp lệ / chưa được khởi tạo).

std::optional độc đáo chăm sóc các vấn đề phát sinh với các giải pháp trước đó.


Tôi biết về cơ bản chúng giống nhau, nhưng tại sao bạn lại sử dụng boost::thay vì std::?
0x499602D2

4
Cảm ơn - Tôi đã sửa nó (Tôi đã sử dụng boost::optionalvì sau khoảng hai năm sử dụng, nó được mã hóa cứng vào vỏ não trước trán của tôi).
utnapistim

1
Điều này gây ấn tượng với tôi như một ví dụ nghèo nàn, vì nếu nhiều khối được hoàn thành thì chỉ có thể trả lại một khối và phần còn lại bị loại bỏ. Thay vào đó, hàm sẽ trả về một tập hợp các khối có thể trống.
Ben Voigt

Bạn đúng; đó là một ví dụ nghèo nàn; Tôi đã sửa đổi ví dụ của mình cho "phân tích cú pháp đầu tiên nếu có".
utnapistim

4

Tôi thường sử dụng các tùy chọn để biểu thị dữ liệu tùy chọn được lấy từ các tệp cấu hình, nghĩa là nơi dữ liệu đó (chẳng hạn như phần tử dự kiến, nhưng không cần thiết, trong tài liệu XML) được cung cấp tùy chọn, để tôi có thể hiển thị rõ ràng và rõ ràng nếu dữ liệu đã thực sự có mặt trong tài liệu XML. Đặc biệt là khi dữ liệu có thể có trạng thái "không được đặt", so với trạng thái "trống" và "đặt" (logic mờ). Với một tùy chọn, thiết lập và không được thiết lập là rõ ràng, cũng sẽ trống với giá trị 0 hoặc null.

Điều này có thể cho thấy giá trị của "không được đặt" không tương đương với "trống". Trong khái niệm, một con trỏ tới int (int * p) có thể hiển thị điều này, trong đó null (p == 0) không được đặt, giá trị 0 (* p == 0) được đặt và trống và bất kỳ giá trị nào khác (* p <> 0) được đặt thành một giá trị.

Ví dụ thực tế, tôi có một phần hình học được lấy từ một tài liệu XML có giá trị gọi là cờ kết xuất, trong đó hình học có thể ghi đè các cờ kết xuất (bộ), vô hiệu hóa cờ kết xuất (được đặt thành 0) hoặc đơn giản là không ảnh hưởng đến các cờ kết xuất (không được đặt), một tùy chọn sẽ là một cách rõ ràng để thể hiện điều này.

Rõ ràng một con trỏ tới một int, trong ví dụ này, có thể hoàn thành mục tiêu, hoặc tốt hơn, một con trỏ chia sẻ vì nó có thể cung cấp triển khai sạch hơn, tuy nhiên, tôi sẽ tranh luận về sự rõ ràng của mã trong trường hợp này. Là một null luôn luôn là "không được thiết lập"? Với một con trỏ, nó không rõ ràng, vì null có nghĩa đen là không được phân bổ hoặc tạo, mặc dù nó có thể , nhưng có thể không nhất thiết có nghĩa là "không được đặt". Cần phải chỉ ra rằng một con trỏ phải được giải phóng và trong thực tế tốt được đặt thành 0, tuy nhiên, giống như với một con trỏ dùng chung, một tùy chọn không yêu cầu dọn dẹp rõ ràng, do đó không có vấn đề gì về việc trộn lẫn dọn dẹp với các tùy chọn chưa được thiết lập.

Tôi tin rằng đó là về sự rõ ràng của mã. Sự rõ ràng làm giảm chi phí bảo trì mã và phát triển. Một sự hiểu biết rõ ràng về ý định mã là vô cùng có giá trị.

Sử dụng một con trỏ để thể hiện điều này sẽ yêu cầu quá tải khái niệm về con trỏ. Để biểu thị "null" là "không được đặt", thông thường bạn có thể thấy một hoặc nhiều nhận xét thông qua mã để giải thích ý định này. Đó không phải là một giải pháp tồi thay vì một tùy chọn, tuy nhiên, tôi luôn chọn cách thực hiện ngầm thay vì nhận xét rõ ràng, vì các bình luận không thể thi hành được (chẳng hạn như bằng cách biên dịch). Ví dụ về các mục ngầm định này để phát triển (những bài viết trong quá trình phát triển được cung cấp hoàn toàn để thực thi ý định) bao gồm các phôi kiểu C ++ khác nhau, "const" (đặc biệt là về các hàm thành viên) và loại "bool", để đặt tên cho một số. Có thể cho rằng bạn không thực sự cần các tính năng mã này, miễn là mọi người tuân theo ý định hoặc nhận xét.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.