首页 > 编程笔记 > C++笔记 阅读:3

C++ 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::optional 是一种更好的方法,这是因为:
但是,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。

相关文章