如何使用std::optional?

174

我正在阅读std::experimental::optional的文档,对它的作用有所了解,但我不明白何时和如何使用它。该网站目前没有任何示例,这使我难以真正理解这个对象的概念。什么情况下使用std::optional是一个好选择,并且它如何弥补在之前的标准中找不到的东西(C++11)。


19
《boost.optional文档》或许可以解释清楚。 - juanchopanza
似乎std :: unique_ptr通常可以用于相同的用例。我猜如果你对new有意见,那么optional可能更可取,但在我看来,那种对new持有这种观点的(开发人员|应用程序)是少数... AFAICT,optional不是一个好主意。至少,我们大多数人可以舒适地没有它而生活。就个人而言,在一个我不必选择unique_ptr vs. optional的世界里,我会感到更加舒适。叫我疯狂,但Python的禅宗是正确的:让有一种正确的方法来做某事! - allyourcode
19
有时我们不想在堆上分配需要删除的东西,所以unique_ptr并不能替代optional。 - Krum
5
没有指针可以替代 "optional"。想象一下,如果你想要一个 "optional<int>" 或者 "<char>",你真的认为需要动态分配、解引用,然后删除这些东西是很“Zen”的吗?这本来不需要任何分配,可以紧凑地放在栈上甚至寄存器中。 - underscore_d
1
另一个区别,毫无疑问是由于所包含值的堆栈分配与堆分配不同,即在std::optional的情况下,模板参数不能是不完整类型(而对于std::unique_ptr则可以)。更准确地说,标准要求_T[...]应满足可销毁的要求。 - dan_din_pantelimon
这是一篇来自微软博客的非常棒的关于此话题的文章:https://devblogs.microsoft.com/cppblog/stdoptional-how-when-and-why/. - Gabriel Staples
4个回答

211

我能想到的最简单的例子:

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(或不需要),这种方式更加合理!

同时,它还能消除“诅咒”的问题,即“如果您不想设置限制,请为 max_count 传递 -1”或“如果您不想设置最小分数,请为 min_match_score 传递 std::numeric_limits<double>::min() ”!


另一个例子:

std::optional<int> find_in_string(std::string s, std::string query);
如果查询字符串不在中,我希望是“没有<代码>int”,而不是为此目的使用的任何特殊值(-1?)。
要获取更多示例,您可以查看boost::optional 文档。 boost::optionalstd::optional 在行为和用法方面基本相同。

14
@gnzlbg std::optional<T> 只是一个 T 和一个 bool。成员函数的实现非常简单。在使用它时,性能不应该是一个问题 - 有时候某些东西是可选的,在这种情况下,这通常是正确的工具。 - Timothy Shields
8
@TimothyShields std::optional<T>比那复杂得多。它使用放置new和很多其他东西,具有适当的对齐和大小,以使其成为字面类型(即可与constexpr一起使用),以及其他方面。天真的Tbool方法很快就会失败。 - Rapptz
18
第256行代码定义了一个名为storage_t的联合体,其中包括一个未命名的unsigned char变量和一个名为value_的T类型变量。第289行代码定义了一个结构体optional_base,其中包括一个bool类型变量init_、一个名为storage_类型为storage_t<T>的变量等。从概念上和具体实现来看,这种设计就是由一个T类型的变量和一个bool类型的变量组成,但它的实现非常棘手和复杂。作者认为,如果只采用朴素的Tbool类型的方法,那么很快就会出现问题。你如何看待这段代码,才能得出这样一种结论呢? - Timothy Shields
15
@Rapptz 这段代码仍然存储了一个布尔值和一个整数的空间。这个联合体只是为了让可选项在不需要实际使用T时不会构造一个T。它仍然是struct{bool,maybe_t<T>},联合体只是避免了使用struct{bool,T},因为这样会在所有情况下都构造一个T。 - PeterT
14
很好的问题。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
显示剩余13条评论

41

以下是从最新通过的文件: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
         }
     }
}

14
因为你可以将信息在调用层次结构中向上传递,表明是否得到了一个int,而不是传递一些“幻影”值,假设其具有“错误”的含义。 - Luis Machuca
1
@Wiz 这实际上是一个很好的例子。它 (A) 允许 str2int() 以任何希望的方式实现转换,(B) 不管获取 string s 的方法如何,(C) 通过 optional<int> 传达完整的含义,而不是一些愚蠢的魔法数字、bool/引用或基于动态分配的方式来完成它。 - underscore_d

11

但我不明白应该在什么情况下使用它,以及如何使用它。

考虑当您编写 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很好地解决了之前解决方案中出现的问题。


1
我知道它们基本上是一样的,但你为什么要使用 boost:: 而不是 std:: 呢? - David G
4
谢谢 - 我进行了更正(我使用 boost::optional 是因为使用了大约两年后,它已经编码进了我的前额皮质)。 - utnapistim
1
这个例子让我感到不太好,因为如果多个块被完成,只有一个会被返回,其余的将被丢弃。该函数应该返回一个可能为空的块集合。 - Ben Voigt
你是对的;那个例子很差;我已经修改了关于“如果可用则解析第一个块”的例子。 - utnapistim

6
我经常使用可选项来表示从配置文件中提取的可选数据,也就是说,该数据(例如 XML 文档中的预期但非必要元素)是可选提供的,这样我可以明确、清晰地显示数据是否实际存在于 XML 文档中。特别是当数据可以有“未设置”状态时,与“空”和“设置”状态(模糊逻辑)相比。使用可选项,设置和未设置是清晰的,值为0或null时,空也是清晰的。
这可以展示“未设置”的值不等同于“空”。在概念上,指向 int 的指针(int* p)可以展示这一点,其中 null(p == 0)未设置,值为 0(*p == 0)设置为空,任何其他值(*p <> 0)都设置为一个值。
作为一个实际的例子,在一个 XML 文档中提取的几何形状中有一个名为“渲染标志”的值,其中几何形状可以覆盖渲染标志(设置),禁用渲染标志(设置为0),或者根本不影响渲染标志(未设置),可选项将是一种清晰的方式来表示它。
显然,在这个例子中,使用指向 int 的指针可以实现这个目标,或者更好的是,使用共享指针,因为它可以提供更清晰的实现,但是,在这种情况下,我认为它关乎代码的清晰性。 null 总是表示“未设置”吗?使用指针不够清晰,因为 null 的字面意思是未分配或未创建,尽管它可能,但未必意味着“未设置”。值得指出的是,指针必须被释放,并且在良好的实践中设置为 0,然而,与共享指针一样,可选项不需要显式清理,因此不存在将清理与未设置的可选项混淆的问题。
我认为这是关于代码的清晰性。清晰度降低了代码维护和开发的成本。对代码意图的清晰理解非常有价值。
使用指针来表示这个会要求过载指针的概念。通常,要表示“null”为“未设置”,您可能会在代码中看到一个或多个注释来解释这个意图。这并不是一个坏的解决方案,而不是可选项。然而,我总是选择隐式实现而不是显式注释,因为注释是不可强制执行的(例如通过编译)。这些用于开发的隐式项目(仅为了强制执行意图而提供的开发文章)的示例包括各种 C++ 样式转换、“const”(特别是对于成员函数)和“bool”类型,仅举几例。可以说您实际上不需要这些代码功能,只要每个人都遵守意图或注释即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接