C++ std::optional类模板用法详解(附带实例)
有时,我们需要在值可用时存储它,在值不可用时存储一个空值。这种情况的一个典型例子是函数的返回值,函数可能无法产生返回值,但这种失败不是错误。
例如,假设有一个函数通过指定键从字典中查找并返回值,这很可能会出现没有找到值的情况,因此函数要么返回一个布尔值(如果需要更多的错误代码,则返回一个整数值)并且有一个引用参数来保存返回值,要么返回一个指针(原始指针或智能指针)。
在 C++17 中,std::optional 是这些解决方案的最佳选择,std::optional 类模板是一个模板容器,用于存储一个可能存在也可能不存在的值。
std::optional<T> 类模板是基于 boost::optional 设计的,在
1) 要存储值,可以使用构造函数或将值直接赋给 std::optional 对象:
2) 要读取存储的值,可以使用 operator* 或 operator->:
3) 读取存储的值还可以使用成员函数 value() 和 value_or():
4) 要检查容器是否存储值,可以使用 bool 的转换操作符或成员函数 has_value():
5) 要修改存储的值,可以使用成员函数 emplace()、reset() 或 swap():
使用 std::optional 可以获得:
1) 可能无法生成值的函数的返回值:
2) 可选的函数形参:
3) 可选的类数据成员:
类模板 std::optional 在概念上是这样实现的:
optional 类型(在其他编程语言中称为可空类型)的典型用途是作为可能返回失败的函数的返回类型。这种情况的可能解决方案包括:
类模板 std::optional 是一种更好的方法,这是因为:
但是,optional 对象也可以用于类数据成员,并且编译器能够优化内存布局以实现高效存储。
类模板 std::optional 不能用于返回多态类型。例如,如果要编写一个需要从类型层次结构返回不同类型的工厂方法,则不能依赖 std::optional 并且需要返回一个指针,最好是 std::unique_ptr 或 std::shared_ptr(取决于对象的所有权是否需要共享)。
当使用 std::optional 将可选参数传递给函数时,需要明白它可能会创建副本,如果涉及大型对象,这可能会导致性能问题。
让我们考虑下面的函数,这个函数有对 std::optional 参数的常量引用:
如果 bar 是一个小对象,那么它不会引起很大的关注,但如果它是大型对象,那么可能会产生一个性能问题。避免这种情况的解决方案取决于上下文,可能涉及创建第二个重载(使该重载使用对 bar 的常量引用)或者要完全避免使用 std::optional。
例如,假设有一个函数通过指定键从字典中查找并返回值,这很可能会出现没有找到值的情况,因此函数要么返回一个布尔值(如果需要更多的错误代码,则返回一个整数值)并且有一个引用参数来保存返回值,要么返回一个指针(原始指针或智能指针)。
在 C++17 中,std::optional 是这些解决方案的最佳选择,std::optional 类模板是一个模板容器,用于存储一个可能存在也可能不存在的值。
std::optional<T> 类模板是基于 boost::optional 设计的,在
<optional>
头文件中可用。如果你熟悉 boost::optional 并在代码中使用过它,那么可以无缝地迁移到 std::optional。C++ std::optional使用方式
我们可以使用以下操作来处理 std::optional:1) 要存储值,可以使用构造函数或将值直接赋给 std::optional 对象:
std::optional<int> v1; // v1 is empty std::optional<int> v2(42); // v2 contains 42 v1 = 42; // v1 contains 42 std::optional<int> v3 = v2; // v3 contains 42
2) 要读取存储的值,可以使用 operator* 或 operator->:
std::optional<int> v1{ 42 }; std::cout << *v1 << '\n'; // 42 std::optional<foo> v2{ foo{ 42, 10.5 } }; std::cout << v2->a << ", " << v2->b << '\n'; // 42, 10.5
3) 读取存储的值还可以使用成员函数 value() 和 value_or():
std::optional<std::string> v1{ "text"s }; std::cout << v1.value() << '\n'; // text std::optional<std::string> v2; std::cout << v2.value_or("default"s) << '\n'; // default
4) 要检查容器是否存储值,可以使用 bool 的转换操作符或成员函数 has_value():
struct foo { int a; double b; }; std::optional<int> v1{ 42 }; if (v1) std::cout << *v1 << '\n'; std::optional<foo> v2{ foo{ 42, 10.5 } }; if (v2.has_value()) std::cout << v2->a << ", " << v2->b << '\n';
5) 要修改存储的值,可以使用成员函数 emplace()、reset() 或 swap():
std::optional<int> v{ 42 }; // v contains 42 v.reset(); // v is empty
使用 std::optional 可以获得:
1) 可能无法生成值的函数的返回值:
template <typename K, typename V> std::optional<V> find(int const key, std::map<K, V> const & m) { auto pos = m.find(key); if (pos != m.end()) return pos->second; return {}; } std::map<int, std::string> m{ { 1, "one"s }, { 2, "two"s }, { 3, "three"s } }; auto value = find(2, m); if (value) std::cout << *value << '\n'; // two value = find(4, m); if (value) std::cout << *value << '\n';
2) 可选的函数形参:
std::string extract(std::string const & text, std::optional<int> start, std::optional<int> end) { auto s = start.value_or(0); auto e = end.value_or(text.length()); return text.substr(s, e - s); } auto v1 = extract("sample"s, {}, {}); std::cout << v1 << '\n'; // sample auto v2 = extract("sample"s, 1, {}); std::cout << v2 << '\n'; // ample auto v3 = extract("sample"s, 1, 4); std::cout << v3 << '\n'; // amp
3) 可选的类数据成员:
struct book { std::string title; std::optional<std::string> subtitle; std::vector<std::string> authors; std::string publisher; std::string isbn; std::optional<int> pages; std::optional<int> year; };
C++ std::optional工作原理
类模板 std::optional 是一个类模板,它表示可选值的容器。如果容器确实有值,则该值存储为 optional 对象的一部分,不涉及堆分配和指针。类模板 std::optional 在概念上是这样实现的:
template <typename T> class optional { bool _initialized; std::aligned_storage<sizeof(T), alignof(T)> _storage; };模板别名 std::aligned_storage_t 允许我们创建未初始化的内存块,用于保存给定类型的对象。如果类模板 std::optional 是默认构造的、复制构造的,或者是从另一个空 optional 对象或 std::nullopt_t 值复制赋值的,则该类模板不包含值。这是一个辅助类型,实现为一个空类,它指示一个状态为未初始化的 optional 对象。
optional 类型(在其他编程语言中称为可空类型)的典型用途是作为可能返回失败的函数的返回类型。这种情况的可能解决方案包括:
- 返回 std::pair<T,bool>,其中 T 是返回值的类型,第二个元素是布尔标志,它指示第一个元素的值是否有效;
- 返回 bool 类型,接受一个类型为 T& 的额外形参,只在函数返回成功时为该形参赋值;
- 返回原始指针或智能指针类型,并使用 nullptr 表示函数返回失败。
类模板 std::optional 是一种更好的方法,这是因为:
- 一方面,它不涉及函数的输出参数(这对于返回值来说是不正常的),不需要使用指针;
- 另一方面,它更好地封装了std::pair<T,bool> 的详细信息。
但是,optional 对象也可以用于类数据成员,并且编译器能够优化内存布局以实现高效存储。
类模板 std::optional 不能用于返回多态类型。例如,如果要编写一个需要从类型层次结构返回不同类型的工厂方法,则不能依赖 std::optional 并且需要返回一个指针,最好是 std::unique_ptr 或 std::shared_ptr(取决于对象的所有权是否需要共享)。
当使用 std::optional 将可选参数传递给函数时,需要明白它可能会创建副本,如果涉及大型对象,这可能会导致性能问题。
让我们考虑下面的函数,这个函数有对 std::optional 参数的常量引用:
struct bar { /* details */ }; void process(std::optional<bar> const & arg) { /* do something with arg */ } std::optional<bar> b1{ bar{} }; bar b2{}; process(b1); // no copy process(b2); // copy construction第一次调用 process() 不涉及任何额外的对象构造,因为我们传递了一个 std::optional<bar><bar> 对象。但是,第二次调用将涉及 bar 对象的复制构造,因为 b2 是一个 bar 对象,需要被复制到 std::optional<bar>,即使 bar 实现了移动语义,也会进行复制。
如果 bar 是一个小对象,那么它不会引起很大的关注,但如果它是大型对象,那么可能会产生一个性能问题。避免这种情况的解决方案取决于上下文,可能涉及创建第二个重载(使该重载使用对 bar 的常量引用)或者要完全避免使用 std::optional。