如何使用 std::optional ?

我正在阅读 std::experimental::optional的文档,我对它的作用有一个很好的想法,但是我不理解 什么时候,我应该使用它或者我应该如何使用它。这个网站还没有包含任何例子,这让我更难掌握这个对象的真正概念。什么时候使用 std::optional是一个很好的选择,它如何弥补以前的标准(C + + 11)中没有找到的东西。

121817 次浏览

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

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

如果查找中没有特定的键,那么我们可以简单地返回“ no value”

我可以这样使用它:

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(或不采用)的所有可能组合)要有意义得多!

它也 排除了被诅咒了“通过 -1max_count,如果你不想要一个限制”或“通过 std::numeric_limits<double>::min()min_match_score,如果你不想要一个最低分”!


另一个例子:

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

如果查询字符串不在 s中,我希望“ no int”—— 没有,不管有人为此决定使用什么特殊值(- 1?).


有关其他示例,您可以查看 boost::optional 文件boost::optionalstd::optional在行为和使用方面基本相同。

新通过的文件: 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
}
}
}

但是我不知道什么时候该用,或者怎么用。

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

我经常使用选项来表示从配置文件中提取的可选数据,也就是说可选地提供数据(例如 XML 文档中预期但不必要的元素)的位置,这样我就可以明确清楚地显示数据是否实际存在于 XML 文档中。特别是当数据可能具有“未设置”状态,而不是“空”和“设置”状态(模糊逻辑)时。使用可选的 set 和 not set 是 clear,也可以使用空值0或 null 表示 clear。

这可以显示“ not set”的值不等同于“ void”。在概念上,指向 int (int * p)的指针可以表明这一点,其中没有设置 null (p = = 0) ,设置0(* p = = 0)的值为空,任何其他值(* p < > 0)都设置为一个值。

作为一个实际的例子,我从一个 XML 文档中提取了一段几何图形,它有一个称为呈现标志的值,其中几何图形可以覆盖呈现标志(设置) ,禁用呈现标志(设置为0) ,或者干脆不影响呈现标志(未设置) ,一个可选的方式可以清楚地表示这一点。

很明显,在这个例子中,指向 int 的指针可以实现目标,或者更好的,共享指针,因为它可以提供更清晰的实现,但是,我认为在这个例子中,它是关于代码清晰度的。Null 总是“未设置”吗?对于指针,它是不清楚的,因为 null 字面上的意思是没有分配或创建,虽然它是 可以,但是 不一定可能意味着“没有设置”。值得指出的是,必须释放一个指针,并且在良好的实践中,设置为0,但是,像一个共享指针一样,一个可选的不需要显式的清理,所以不需要担心混淆清理与可选的没有设置。

我相信这是代码清晰度的问题。清晰度降低了代码维护和开发的成本。对代码意图的清晰理解是非常有价值的。

使用指针来表示这一点需要重载指针的概念。要将“ null”表示为“ not set”,通常可以通过代码看到一个或多个注释来解释这一意图。这是一个不错的解决方案,而不是一个可选的,但是,我总是选择隐式实现而不是显式注释,因为注释是不可执行的(例如通过编译)。这些用于开发的隐式项目(开发中纯粹为了强制实现意图而提供的文章)的例子包括各种 C + + 样式强制类型转换、“ const”(特别是在成员函数上)和“ bool”类型等等。可以说,您并不真正需要这些代码特性,只要每个人都遵守意图或注释。