目录
区分std::nullopt与std::nullptr —— 区别与联系
在现代 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提供了以下优点:
- 简洁直观,代码可读性更高 —— 明确表达了函数的返回值可能不存在的情况
- 减少空指针异常和其他错误,因为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 是否包含值 | 如果包含值,返回 |
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 可以避免初始化,直到需要时才构造。
示例代码:延迟初始化数据库对象
假设我们有一个程序需要和数据库连接,但数据库连接并不是一开始就建立的,而是需要在某个条件满足时才进行连接(延迟初始化)。同时,在没有初始化时,我们要确保程序不会因为访问未初始化的数据库连接而出错。
主要功能:
- 数据库连接类
- 表示与数据库的连接,在创建时提供一个连接字符串
- 提供了 query 方法,模拟向数据库发送查询
- 应用程序类
- 包含
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的不同之处,熟悉其常用接口。