Bootstrap

C++标准库中的灵活工具 —— 探索std::optional的妙用

目录

问题引出

何为std::optional

如何使用std::optional —— 常用接口介绍

std::optional的其他用法——延迟初始化

示例代码:延迟初始化数据库对象

区分std::nullopt与std::nullptr —— 区别与联系

std::optional的常见误区

总结

在现代 C++ 中,std::optional是一个功能强大且灵活的工具,帮助显著提高代码的安全性和可读性。它不仅能够帮助开发者简洁地表达“值可选”的场景,还能实现变量的延迟初始化。在这篇文章中,我们将通过若干实例深入探讨std::optional的常见应用场景和核心接口函数,以及如何用它实现延迟初始化。此外,我们还会分析一些使用std::optional时的常见错误,帮助你避免陷阱并更好地在项目中应用这一工具。

问题引出

思考这样一个实际例子:假设现在有一个给定的C++数组(用vector表示),你需要编写一个函数在这个数组中搜索某个元素(用element表示)并返回它的位置。

这个问题的一般解法是:遍历这个数组中的元素,逐个与给定的元素值相比较。如果找到该元素,则返回元素的位置,如果没有找到,则用-1表示。代码如下:

#include <vector>

int findElement(const std::vector<int>& container, int element) {
    for (int i = 0; i < container.size(); ++i) {
        if (container[i] == element) {
            return i; // 找到该元素,返回元素所在的位置index
        }
    }
    return -1; // 元素未找到,返回-1
}

这种做法非常简洁明了,但是缺点在于需要依赖特殊值(在此为-1)来表示“未找到”这一特殊情况。在std::optional被引入以前,通常的做法是使用0,-1或std::nullptr来表示这种特殊情况,但是这些这种做法也存在产生歧义的可能。如果一时忘记了这种特殊规定,则可能产生无法预料的错误。

何为std::optional

std::optional是在 C++17 中引入的一种模板类型,模板类为std::optional<T>。它提供了一种高效的方式来表示“可选”值,意味着一个变量可能有值,也可能没有值

回到上面的例子,如果使用std::optional来改写上面的代码,则代码如下:

#include <optional>
#include <vector>

std::optional<size_t> findElement(const std::vector<int>& container, int element) {
    for (size_t i = 0; i < container.size(); ++i) {
        if (container[i] == element) {
            return i; // 找到该元素,返回元素所在的位置index
        }
    }
    return std::nullopt; // 无法找到元素,返回空
}

与传统的处理方式相比,std::optional提供了以下优点:

  1. 简洁直观,代码可读性更高 —— 明确表达了函数的返回值可能不存在的情况
  2. 减少空指针异常和其他错误,因为std::optional对象明确声明其存在或不存在的状态

如何使用std::optional —— 常用接口介绍

上文的例子解释了std::optional可以用来描述一个值可能存在(也可能不存在),下面再举一个例子来简要介绍std::optional<T>模版类提供的常用接口。

#include <iostream>
#include <optional>
#include <vector>

std::optional<size_t> findElement(const std::vector<int>& container, int target) {
    for (size_t i = 0; i < container.size(); ++i) {
        if (container[i] == target) {
            return i;
        }
    }
    return std::nullopt;
}

int main() {
    std::vector<int> data = {10, 20, 30, 40};
    auto result = findElement(data, 30);

    if (result.has_value()) {
        std::cout << "元素对应的位置为: " << result.value() << std::endl;
    } else {
        std::cout << "没有找到元素" << std::endl;
    }

    return 0;
}

has_value用于检查 std::optional 是否包含值,value用于获取值。

实际例子:假设程序有一个可选的线程数配置。如果用户没有指定线程数,默认值为4。

#include <iostream>
#include <optional>

void configureThreads(const std::optional<int>& threadCount) {
    int threads = threadCount.value_or(4); // 如果未指定,则使用默认值 4
    std::cout << "Configured threads: " << threads << std::endl;
}

int main() {
    std::optional<int> userDefinedThreads = 8;
    configureThreads(userDefinedThreads); // 输出:Configured threads: 8

    std::optional<int> noThreads; // 用户未指定
    configureThreads(noThreads); // 输出:Configured threads: 4

    return 0;
}

value_or在 std::optional 有值时返回该值,否则返回指定的默认值。这种用法避免了显式检查 has_value 并提供默认值的额外代码。

除此以外,其他常用接口也一并整理在下表中。

常用接口一览表
接口功能说明
std::optional创建空的 optional 或直接赋值默认构造时为空,也可以直接初始化值
has_value()检查 optional 是否包含值

如果包含值,返回 true,否则返回 false

value()获取 optional 中的值,如果不存在值会抛出异常如果 optional 为空,抛出 std::bad_optional_access
value_or(default)如果有值则返回该值,否则返回提供的默认值用于提供默认值,避免显式检查是否有值
swap()交换两个 optional 的值如果两个 optional 都有值,则交换它们的值;如果一个为空,则交换空值状态。
reset()清除 optional 中的值,使其为空之后 optional 变为无值状态
emplace(args...)直接在 optional 中构造一个值,避免多余的拷贝或移动如果之前有值,会先清除之前的值,然后构造新的值
operator==/!=比较两个 optional 对象或 optional 和普通值如果两个 optional 都有值,则比较值;如果都为空,则视为相等
operator</>比较 optional 对象的值如果两者都有值,则比较值;如果一者为空,空值视为较小。
operator*()获取值的快捷方式,类似指针解引用必须确保 optional 有值,否则会产生未定义行为

std::optional的其他用法——延迟初始化

延迟初始化:推迟对象的创建或资源的初始化,直到第一次需要该对象或资源时才进行初始化。延迟初始化的主要目的是优化资源使用,提高程序运行效率

在程序设计时,我们往往会遇到这样一种情况:某些类对象没有默认构造函数,只能通过参数化构造。如果用普通成员变量,必须使用动态分配;但使用std::optional 可以避免初始化,直到需要时才构造。

示例代码:延迟初始化数据库对象

假设我们有一个程序需要和数据库连接,但数据库连接并不是一开始就建立的,而是需要在某个条件满足时才进行连接(延迟初始化)。同时,在没有初始化时,我们要确保程序不会因为访问未初始化的数据库连接而出错。

主要功能:

  1. 数据库连接类
    1. 表示与数据库的连接,在创建时提供一个连接字符串
    2. 提供了 query 方法,模拟向数据库发送查询
  2. 应用程序类
    1. 包含 std::optional<DatabaseConnection> 成员变量,延迟初始化数据库连接
#include <iostream>
#include <optional>
#include <string>

class DatabaseConnection {
public:
    explicit DatabaseConnection(const std::string& connectionString) 
        : connectionString_(connectionString) {
        std::cout << "已连接到数据库,连接信息为:" << connectionString_ << std::endl;
    }

    void query(const std::string& sql) {
        std::cout << "正在执行查询:" << sql << ",连接信息:" << connectionString_ << std::endl;
    }

private:
    std::string connectionString_;
};

class Application {
public:
    void initializeDatabase(const std::string& connectionString) {
        dbConnection_.emplace(connectionString); // 延迟初始化
    }

    void executeQuery(const std::string& sql) {
        if (dbConnection_) {
            dbConnection_->query(sql); // 使用数据库连接
        } else {
            std::cout << "数据库尚未初始化!" << std::endl;
        }
    }

private:
    std::optional<DatabaseConnection> dbConnection_; // 可选的延迟初始化对象
};

int main() {
    Application app;

    // 数据库未初始化时尝试使用
    app.executeQuery("SELECT * FROM users");

    // 延迟初始化数据库连接
    app.initializeDatabase("Server=127.0.0.1;Database=TestDB;User=admin;Password=1234;");
    app.executeQuery("SELECT * FROM users");

    return 0;
}

代码运行结果如下:

数据库尚未初始化!
已连接到数据库,连接信息为:Server=127.0.0.1;Database=TestDB;User=admin;Password=1234;
正在执行查询:SELECT * FROM users,连接信息:Server=127.0.0.1;Database=TestDB;User=admin;Password=1234;

在这个实例中,由于DatabaseConnection无法默认构造,它作为Application的成员变量时无法简单声明,否则编译器将报错。

class Application {
private:
    DatabaseConnection dbConnection; // 错误,DatabaseConnection 没有默认构造函数
};

在这个例子中,可以使用智能指针代替optional来实现延迟初始化的目的,但是智能指针将在堆中开辟内存空间,引入了额外的复杂性来解决内存管理的问题。

区分std::nullopt与std::nullptr —— 区别与联系

在 C++ 中,nullopt与nullptr都可以表示“没有值”或“无效”的状态,但它们的用途和含义并不相同:

nullptr —— 用于指针相关的场景,表明指针为空,即指针不指向任何有效地址

nullopt —— 是std::optional头文件里提供的一个常量,用来表示没有存储任何值的状态

std::optional的常见误区

1. 不能用于处理异常 —— 值的存在与否与是否抛出了异常没有必然联系,std::optional不建议用于处理异常或者返回错误,处理异常可以考虑使用std::expected。 【参考用法:std::expected

2. 忘记检查std::optional是否存在值 —— 在没有赋值的情况下直接调用value函数将造成未定义行为

3. 模版类型<T>中的类型不能是地址 —— 不能使用optional<T&>。

总结

本文从一个实际例子出发,深入浅出地介绍了std::optional的几种常用情形,作为C++17的新特性,它不仅可以描述单一变量的值是否存在,从而在提高代码的可读性的基础上,降低产生潜在错误的可能,还可以用于延迟初始化,对于某些没有默认构造函数的成员变量,std::optional可以巧妙地帮助他们绕开编译器错误,实现延迟初始化的目的。在实际使用过程中,要注意区分std::nullopt与std::nullptr的不同之处,熟悉其常用接口。

;