标准::可选:如何,何时,为什么

这个帖子是一系列的文章的一部分,这里的C++产品团队在微软和其他客人回答我们收到的客户的问题。这些问题可以是任何C++相关的:MSVC工具集,标准语言和库,C++标准委员会,ISOCPP.ORG,CppCon等等。今天的帖子是Casey Carter。

null

C++ 17 添加几个新的 ““词汇类型” – 用于不同来源组件之间接口的类型-到标准库。 MSVC公司 已发布的实现 std::optional , std::any ,和 std::variant 自VisualStudio2017发布以来,我们还没有提供任何关于如何以及何时使用这些词汇表类型的指导。这篇关于 std::optional 本系列将依次检查每种词汇类型。

需要“有时一件事”

如何编写一个可选地接受或返回对象的函数?传统的解决方案是选择一个潜在的价值观,作为哨兵,以表明未来的发展 缺席 价值观:

void maybe_take_an_int(int value = -1); // an argument of -1 means "no value"
int maybe_return_an_int(); // a return value of -1 means "no value"

当该类型的一个可表示值在实践中从未出现时,这种方法就相当有效。如果没有明显的sentinel选项,并且您希望能够传递所有可表示的值,那么就没有那么好了。如果是这样,典型的方法是使用一个单独的 布尔值 要指示可选参数是否包含有效值,请执行以下操作:

void maybe_take_an_int(int value = -1, bool is_valid = false);
void or_even_better(pair<int,bool> param = std::make_pair(-1, false));
pair<int, bool> maybe_return_an_int();

这也是可行的,但很尴尬。“两个不同参数”技术 maybe_take_an_int 要求调用者传递两个事物,而不是一个事物来表示一个概念,并且在调用者忘记这个概念时会默默失败 bool 只是打电话而已 maybe_take_an_int(42) . 使用 pair 另外两个函数避免了这些问题,但是 pair 忘记检查 bool 并可能在 int . 经过 std::make_pair(42, true) std::make_pair(whatever, false) 也和过去有很大的不同 42 或者什么都没有-我们已经使界面难以使用。

“还没有”的需要

如何用初始化延迟的成员对象(即可选地包含对象)编写类?无论出于何种原因,您都不希望在构造函数中初始化此成员。初始化可以在以后的强制调用中进行,也可以仅在请求时进行。当对象被销毁时,仅当成员已初始化时,才必须销毁该成员。可以通过使用 bool 跟踪它的初始化状态,然后做可怕的布局 new 技巧:

using T = /* some object type */;

struct S {
  bool is_initialized = false;
  alignas(T) unsigned char maybe_T[sizeof(T)];

  void construct_the_T(int arg) {
    assert(!is_initialized);
    new (&maybe_T) T(arg);
    is_initialized = true;
  }

  T& get_the_T() {
    assert(is_initialized);
    return reinterpret_cast<T&>(maybe_T);
  }

  ~S() {
    if (is_initialized) {
      get_the_T().~T(); // destroy the T
    }
  }

  // ... lots of code ...
};

这个 "lots of code" 正文中的注释 S 是编写复制/移动构造函数/赋值运算符的位置,根据源对象和目标对象是否包含初始化的 T . 如果这一切对你来说都是可怕的混乱和脆弱,那么就拍拍自己的背吧——你的直觉是对的。我们正沿着悬崖边缘行走,在那里,小错误会让我们陷入不确定的行为。

上述许多问题的另一个可能的解决方案是动态分配“可选”值并通过指针传递它 – 理想的 std::unique_ptr . 考虑到C++程序员习惯于使用指针,这个解决方案具有良好的可用性: 无效的 指针指示无值条件, * 用于访问值, std::make_unique<int>(42) 只是和其他人相比有点尴尬 return 42 unique_ptr 自动为我们处理解除分配。 当然 可用性不是唯一的问题;习惯C++的零开销抽象的读者会立即抓住这个解决方案,抱怨动态分配比简单地返回整数要昂贵很多个数量级。我们想解决这类问题 需要 动态分配。

optional 是 强制性的

C++ 17解决上述问题的方法是 std::optional . optional<T> 直接解决传递或存储当前可能是或可能不是对象的内容时出现的问题。 optional<T> 提供接口以确定它是否包含 T 以及查询存储值。您可以初始化 optional 用实际的 T 值或默认值初始化它(或使用 std::nullopt )把它放在“空”状态。 optional<T> 甚至延伸 T 的订购操作 < , > , <= , >= –一个空的 optional 比任何人都少 optional 包含 T –所以你可以在某些上下文中使用它,就像它是一个 T . optional<T> 存储 T 对象内部,因此动态分配不是必需的,并且事实上被C++标准明确禁止。

我们的函数需要选择性地传递 T 将被宣布为:

void maybe_take_an_int(optional<int> potential_value = nullopt); 
  // or equivalently, "potential_value = {}"
optional<int> maybe_return_an_int();

optional<T> 可以从 T 值,调用方 maybe_take_an_int 除非它们是显式传递的,否则不需要更改 -1 表示“not-a-value.”类似地 maybe_return_an_int 只需要换回来的地方 -1 让“not-a-value”返回 nullopt (或等同地) {} ).

的呼叫者 maybe_return_an_int 以及 maybe_take_an_int 需要更多实质性的改变。您可以显式地询问 optional 使用 has_value 成员或通过上下文转换为 bool :

optional<int> o = maybe_return_an_int();
if (o.has_value()) { /* ... */ }
if (o) { /* ... */ } // "if" converts its condition to bool

一旦你知道 optional 包含一个值,可以用 * 操作员:

if (o) { cout << "The value is: " << *o << ''; }

或者你可以用 价值 获取存储值或 bad_optional_access 如果没有异常,则不必检查:

cout << "The value is: " << o.value() << '';

或者 value_or 成员函数,如果您希望从空的 optional :

cout << "The value might be: " << o.value_or(42) << '';

所有这些加在一起意味着我们不能像“传统”解决方案那样不经意地使用垃圾值。试图访问空 optional 如果使用 value() 成员或未定义的行为(如果通过 * 可由调试库和静态分析工具捕获的运算符。更新“旧”代码可能和替换有效性测试一样简单,比如 value == not_a_value_sentinel if (is_valid) 具有 opt_value.has_value() if (opt_value) 并用 *opt_value .

回到具体的例子,查找给定整数的字符串的函数可以简单地返回 optional<string> . 这样就避免了建议解决方案的问题;我们可以

  • 与“返回默认值”解决方案不同,可以轻松区分无值情况和已找到值情况,
  • 在不使用异常处理机械的情况下报告无价值案例,如果此类案例频繁而非异常,则可能太贵,
  • 避免将实现细节泄漏给调用方,因为这是公开“end”迭代器所必需的,它们可以将返回的迭代器与之进行比较。

解决延迟初始化问题很简单:我们只需添加一个 optional<T> 我们班的成员。标准库实现者负责获得位置 new 正确处理,以及 std::optional 已经处理了复制/移动构造函数/赋值运算符的所有特殊情况:

using T = /* some object type */;

struct S {
  optional<T> maybe_T;    

  void construct_the_T(int arg) {
    // We need not guard against repeat initialization;
    // optional's emplace member will destroy any 
    // contained object and make a fresh one.        
    maybe_T.emplace(arg);
  }

  T& get_the_T() { 
    assert(maybe_T);
    return *maybe_T;    
    // Or, if we prefer an exception when maybe_T is not initialized:
    // return maybe_T.value();
  }

  // ... No error-prone handwritten special member functions! ...
};

optional 特别适合于延迟初始化问题,因为它本身就是延迟初始化的实例。所包含的 T 可以在构造时初始化,也可以稍后初始化,或者从不初始化。任何包含 T 必须在 optional 被摧毁了。设计人员 optional 我已经回答了这方面出现的大多数问题。

结论

任何时候,你需要一个工具来表达“价值与否”,或“可能的答案”,或“延迟对象” 初始化”,你应该进入工具箱 std::optional . 为这些情况使用词汇表类型可以提高抽象级别,使其他人更容易理解您的代码在做什么。声明 optional<T> f(); void g(optional<T>); 表达意图比表达意图更清楚、更简洁 pair<T, bool> f(); void g(T t, bool is_valid); . 就像单词的情况一样,在我们的词汇表中添加类型会增加我们简单描述复杂问题的能力 – 它使我们更有效率。

如果你有任何问题,请随时回答 在下面发表评论。 您也可以发送 任何 通过电子邮件直接向作者提出意见和建议 cacarter@microsoft.com ,或Twitter@CoderCasey。谢谢您!

© 版权声明
THE END
喜欢就支持一下吧,技术咨询可以联系QQ407933975
点赞0 分享