我在滥用std :: optional

时间:2019-06-20 06:40:50

标签: c++ c++17 stdoptional

std::optional如何适合我的代码?

我的代码包含许多看起来像这样的函数:

bool GetName(_Out_ string& strRes)
{
   // try to get/calculate the value
   // value may also be empty
   // return true on success, false otherwise.
}

现在我找到了std::optional,我只需编写代码:

std::optional<std::string> GetName()

为什么或何时不使用std::optional

  1. 我知道使用std::optional会带来性能价格,并且为了争辩,让我们假设我愿意支付。
  2. 我也理解编码如下: std::optional<int> iVal = 9;毫无意义。

我被std::optional迷住了吗?

我发现std::optional很自然,以至于我得出的结论是,默认情况下是我的许多本机类型,boolint,{ {1}},应声明为string

因此,与其遵循以下前提:

  

仅在绝对需要时使用std::optional

我倾向于:

  

始终{strong>除非使用std::optional,否则您确定现在不需要了,或者   将来。

问题1

以下规则是否有效:

在以下情况下使用std::optional

  1. 在运行时计算出一个值
  2. 计算可能会失败
  3. 您想知道计算是否失败

问题2

如果使用std::optional变得很丰富,我是否有冒着代码可读性问题的风险?


更新

答案和评论使讨论偏离了我最初的关注,这是试图为何时使用std::optional定义经验法则。

我将介绍一些常见的反馈意见

  

您正在编码C样式的错误检查功能。

     

计算失败时使用异常。

提供一个不太好的我的动机例子也许是我的错。 std::optional仅是我常用的案例。 我不想关注错误的反映方式(如果有的话),而是关注是否为可选项命名了一个好候选人?

我没有声明如果GetName()返回false,则暗示我的流程中存在某种错误。当GetName返回false时,我可以生成一个随机名称,也可以忽略当前迭代。无论调用者如何对返回值做出反应,GetNamename的意义上都是optional

我可以提供一个更好的示例:

may not be present

在此函数中,由于我想区分两个容器,因此我被“强制”提供了两个容器:

    在数据库中找到
  1. 键,它为空。该密钥将在void GetValuesFromDB(vector<string> vecKeys, map<string,string> mapKeyValue);
  2. 中 在数据库中找不到
  3. 键。 。该密钥将不在mapKeyValue

但是有了mapKeyValue我可以去:

optional

我相信我们也从字面上理解void GetValuesFromDB(map<string,std::optional<string> > mapKeyValue); 一词。

我真的支持有人可以使用optional If you want to represent a nullable type nicely. Rather than using unique values (like -1, nullptr, NO_VALUE or something)

的观点。

c ++标准委员会可以轻松决定将std::optional命名为std::optional

4 个答案:

答案 0 :(得分:3)

  

我在滥用std :: optional

也许。

std::optional的预期语义是指示某些东西是可选的。也就是说,如果事物不存在,那是可以的,不是错误。

如果您的方法失败,它将返回一个空的可选内容以指示失败。但是此失败是否也是错误取决于调用代码所期望的语义,您没有显示或描述该语义:如果可选结果如何处理?是空的吗?

例如,

auto name = GetName();
if (!name) { diediedie(FATAL_ERROR); }

建议该名称并不是真正的可选名称。如果您不能(如您所说)在此处使用异常,则variant<string, error>方法看起来更清晰,并且还可以让失败的函数传达失败原因,否则将导致失败。

相反

auto name = GetName();
if (name) {
  cout << "User is called " << *name << '\n';
} else {
  cout << "User is anonymous\n";
}

似乎合理,因为未能获取名称不是错误。


  

为什么... 使用std::optional

当您不想传达某些信息是可选时。

显然,您可以滥用它,就像其他任何事物一样,但是它可能会造成混乱,误导,意外和其他不良习惯。

  

我是不是太迷住了……?

也许。如果您的错误处理污染了您的所有代码,则您可能应该考虑更早失败,而不是继续尝试那些永远无法成功的逻辑。

如果您确实有很多可能合法丢失的变量(就程序逻辑而言),那么您可能正确使用了它。

  

问题1

同样,这取决于程序上下文中故障的含义。

  

问题2

是的,如果 everything 是可选的,那么您的代码将是一团乱码,部分是由于视觉噪音,但主要是因为您无法确定逻辑中的哪个点甚至存在变量


您的GetValuesFromDB编辑可能可能可以使用可选的 if ,因为您的数据库相对来说是非结构化的,没有任何约束。您不能依赖于正在填充的任何特定列,所有列都是可选的,您需要手动检查每个字段的存在。它们是真正可选的。

如果您的数据库具有高度的结构性,则对主键使用可选可能是 。您不能在没有主键的情况下没有行,因此使其成为可选键似乎有些奇怪。不过,也许不值得为必不可少的几个列提供一个单独的接口。

  

我相信我们在字面上也可以使用“可选”一词。

我真的无法告诉您您是否读过我的答案-您的评论至少表明您不理解。

假设我们将其称为可为空。我会仍然建议,对于可能合法不存在的事物返回nullable<T>是好的,但是对于缺少指示错误的事物返回相同的类型是错误的。

,您必须确定什么构成代码错误。我们不能为您做到这一点,尤其是提供的信息。那么,您应该对所有内容使用可选吗?也许。这就是我说的。确定您自己的代码的语义和前提条件,您将知道哪些是可选的,哪些是强制性的,哪些不存在应作为错误而不是常规代码路径来处理。

答案 1 :(得分:1)

std::optional是一个类,表示“可选”值。如果遇到某种情况,可能会产生一个值,或者可能不会产生一个值,那么可选是可行的方法。性能价格可以忽略不计(附加条件,如果此处的值实际上可能会使速度变慢,但是由于值是可选的,因此您始终必须检查某个位置,如果仍然存在该值),则实际价格就是内存:

printf("%d\n", (int)sizeof(std::string));
printf("%d\n", (int)sizeof(std::optional<std::string>));

打印:

32
40

使用lang 64位。

所以,如果您要这样做-假设一个结构:

struct A {
    std::optional<std::string> a, b, c, d, e, f, g, h;
};

struct B {
    std::string a, b, c, d, e, f, g, h;
    unsigned char has_a : 1,
        has_b : 1,
        has_c : 1,
        has_d : 1,
        has_e : 1,
        has_f : 1,
        has_g : 1,
        has_h : 1;
};

然后您将获得sizeofs:

A - 320
B - 264

这实际上可能会造成伤害。但通常不。

那么您滥用可选吗?如果您使用可选信号来指示错误,则可能会略有不同,具体取决于错误的上下文。但这总是取决于。如果要让代码清晰起来更重要,那就是该函数没有产生价值,而不是为什么没有产生价值,那么请肯定。

答案 2 :(得分:0)

我会尝试做一个简短的回答:

std::optional的核心只是一个布尔值(ala。std::pair<T, bool>)。请记住,在大多数情况下,原始指针也是可选的。

通常使用以下经验法则:

  1. 对错误/失败状态/例外情况/等使用例外。
  2. 仅在需要时使用可选!除非您是“第二方或第三方”图书馆撰稿人,否则不要计划使用可选的东西来证明自己的未来。
  3. 如果您期望得到结果,请不要使用可选。
  4. 当某些东西实际上是可选的时使用可选的-例如。一个可能最终找到0或1个结果的函数,返回一个可选值是有意义的。可以找到多个结果的函数自然应该倾向于返回结果容器。
  5. 关于字符串,有时更喜欢使用空字符串(主要是由于简单性)而不是std::optional<std::string>,有时反之亦然。

答案 3 :(得分:-4)

我会说这是一种滥用。

我将重写为std::string getName() const(依赖于现在需要的RVO),如果发生不愉快的事情(例如某种获取错误),则引发异常(源自{{1 }})。空名是否是错误取决于您的应用程序。

如果不允许使用异常,则依靠C样式错误传播表格

std::exception

使用int getName(std::string*) const 会使实际错误变得不透明(除非您将错误std::optional施加到当前线程上)。