在一个充满奇思妙想的量子物理世界里,有一个著名的思想实验,被称为“薛定谔的猫”。这个实验由奥地利物理学家埃尔温·薛定谔提出,用以阐述量子力学中一个深奥而迷人的概念——量子叠加态。
故事是这样的:想象有一只猫被关在一个装有少量放射性物质和毒气装置的密封箱子里。这个放射性物质有50%的概率在一个小时内衰变,从而触发毒气装置杀死猫;但同样也有50%的概率不衰变,猫因此安然无恙。根据经典物理学的逻辑,我们自然会认为,当我们打开箱子查看时,猫要么是死的,要么是活的,这两种状态是互斥的。然而,在量子力学的视角下,直到我们打开箱子那一刻之前,猫的状态是处于“既死又活”的叠加态中,这是一个我们无法直接观测到的状态。
现在,让我们将这个思想实验与C++编程语言中的std::optional类型做一个有趣的类比。
std::optional是C++17中引入的一个非常实用的模板类,它提供了一种可能包含或不包含值的包装器。这个类模板提供了一种比传统方法(如使用指针或特殊的“哨兵”值,如空指针或特定值)更安全、更直观的方式来表示可选的或可能不存在的数据。这与“薛定谔的猫”的叠加态有着某种哲学上的相似性。
想象std::optional<T>类型的变量就像那只密封箱子里的猫,其中T代表猫可能存在的两种状态(生或死)之外的“类型”或“存在性”。在std::optional变量被赋予一个值之前,它的状态就像箱子未打开时猫的状态一样,是“未定义”的,即我们不知道里面是否有一个值(猫是死是活)。只有当我们调用.has_value()方法(相当于打开箱子)时,我们才能确定std::optional变量是否包含了一个值(猫是死是活)。
更进一步的,当我们调用.value()或.value_or()方法尝试获取std::optional中的值时,如果它确实包含了一个值,我们就得到了这个值(看到了猫的状态);但如果没有值(即处于“未定义”状态),.value()会抛出一个异常(类似于在量子力学实验中,如果我们尝试直接测量叠加态下的猫,会破坏其叠加态并导致一个确定的结果,但这个结果并不是我们提前能知道的)。
因此,虽然“薛定谔的猫”与std::optional在物理和编程领域分别代表着截然不同的概念,但它们之间在“状态的不确定性和条件性观测”这一点上,确实展现出了某种有趣的相似性。这种类比不仅丰富了我们对这两个概念的理解,也展示了科学思想在不同领域间跨越界限的魅力。
作用
std::optional 的主要作用是提供一种类型安全的方式来处理可能不存在的值。它避免了使用指针可能导致的空指针解引用错误,同时也避免了使用特殊值(如整数类型的 -1 或 0)作为“哨兵”值来标识空或非空状态时的潜在混淆和错误。
举一个例子:我们通过传入的一个值,返回“Hello World”或者空。我们看看不同方案的实现:
指针
下面getValue方法利用指针是否为空来传递信息。如果不为空,则读取它指向的值。这是一种比较像C语言风格的写法。它的问题是函数内申请空间,函数外判断,提升了代码维护的难度,非常不优雅。
#include <iostream>
#include <string>
std::string* getValue(bool condition) {
if (condition) {
return new std::string("Hello, World!");
} else {
return nullptr;
}
}
int main() {
std::string* value = getValue(true);
if (value) {
std::cout << "Value: " << *value << std::endl;
delete value; // 记得释放内存
} else {
std::cout << "No value" << std::endl;
}
value = getValue(false);
if (value) {
std::cout << "Value: " << *value << std::endl;
delete value; // 记得释放内存
} else {
std::cout << "No value" << std::endl;
}
return 0;
}
特殊值
下面这个方案剔除了指针带来的不安全隐患。但是存在另外一个问题,就是“特殊值”——空串被赋予了特殊使命,这样它就不能作为正常值来被看待。这样的设计缩小的适用面。
#include <iostream>
#include <string>
// 使用空字符串作为哨兵变量
const std::string SENTINEL = "";
std::string getValue(bool condition) {
if (condition) {
return "Hello, World!";
} else {
return SENTINEL;
}
}
int main() {
std::string value = getValue(true);
if (value != SENTINEL) {
std::cout << "Value: " << value << std::endl;
} else {
std::cout << "No value" << std::endl;
}
value = getValue(false);
if (value != SENTINEL) {
std::cout << "Value: " << value << std::endl;
} else {
std::cout << "No value" << std::endl;
}
return 0;
}
其他变量
为了表达“有”和“没有”,以及有的时候的“值”,这两种不同的事物,就可以使用两个变量来表达。下例中std::pair<bool, std::string>的第一个值就是表示“有”或者“没有”;后一个值表示“有”的时候的值。这种方案虽然没啥问题,但是不够优雅。
#include <iostream>
#include <string>
#include <utility> // for std::pair
std::pair<bool, std::string> getValue(bool condition) {
if (condition) {
return {true, "Hello, World!"};
} else {
return {false, ""};
}
}
int main() {
auto result = getValue(true);
if (result.first) {
std::cout << "Value: " << result.second << std::endl;
} else {
std::cout << "No value" << std::endl;
}
result = getValue(false);
if (result.first) {
std::cout << "Value: " << result.second << std::endl;
} else {
std::cout << "No value" << std::endl;
}
return 0;
}
Optional
std::optional 就解决了上述各种方案存在的弊端:
类型安全:避免了使用空指针或特殊值作为“哨兵”值可能导致的类型不安全问题。
语义清晰:明确表示了值可能不存在的情况,提高了代码的可读性和可维护性。
减少错误:减少了因空指针解引用或错误地解释特殊值而导致的错误。
灵活性:提供了一种灵活的方式来处理可选值,而不需要改变函数的签名或引入额外的参数。
#include <iostream>
#include <optional>
#include <string>
std::optional<std::string> getValue(bool condition) {
if (condition) {
return "Hello, World!";
} else {
return std::nullopt;
}
}
int main() {
auto value = getValue(true);
if (value) {
std::cout << "Value: " << *value << std::endl;
} else {
std::cout << "No value" << std::endl;
}
value = getValue(false);
if (value) {
std::cout << "Value: " << *value << std::endl;
} else {
std::cout << "No value" << std::endl;
}
return 0;
}
使用场景
-
函数返回值:当函数可能成功返回一个值,也可能因为某些原因(如未找到数据)而无法返回时,使用
std::optional可以明确表明函数可能不返回任何值。这比返回一个特殊值或使用异常来表示错误更为直观和类型安全。 -
配置选项:在处理配置或选项时,某些值可能是可选的。使用
std::optional可以清晰地表示哪些选项已被明确设置,哪些则未被设置。
下面这个例子我们模拟从配置文件中读取字段,保存到AppConfig对象中。我们让AppConfig类的成员使用std::optional封装,这样就可以表达这个变量是否被读取出来。
#include <iostream>
#include <optional>
#include <string>
class AppConfig {
public:
void setDatabaseUrl(const std::string& url) {
databaseUrl = url;
}
void setLogLevel(const std::string& level) {
logLevel = level;
}
std::optional<std::string> getDatabaseUrl() const {
return databaseUrl;
}
std::optional<std::string> getLogLevel() const {
return logLevel;
}
private:
std::optional<std::string> databaseUrl;
std::optional<std::string> logLevel;
};
int main() {
AppConfig config;
// 设置数据库URL
config.setDatabaseUrl("mysql://localhost:3306/mydb");
// 未设置日志级别
// 获取并处理配置项
if (auto dbUrl = config.getDatabaseUrl()) {
std::cout << "Database URL: " << *dbUrl << std::endl;
} else {
std::cout << "Database URL not set." << std::endl;
}
if (auto logLevel = config.getLogLevel()) {
std::cout << "Log Level: " << *logLevel << std::endl;
} else {
std::cout << "Log Level not set." << std::endl;
}
return 0;
}
- 数据库查询:从数据库查询数据时,某些字段可能不存在或为空。使用
std::optional可以直接表示这种情况,而无需担心将空值错误地解释为有效数据。
下面这个例子中:
User 结构体:包含用户信息,其中 middleName 是一个 std::optionalstd::string,表示中间名是可选的。
queryUserFromDatabase 函数:模拟从数据库查询用户信息,根据 userId 返回不同的用户数据或 std::nullopt 表示用户不存在。
printUserInfo 函数:打印用户信息,检查 middleName 是否存在并输出相应的消息。
main 函数:展示了查询不同用户并打印其信息的过程,处理用户存在和不存在的情况。
#include <iostream>
#include <optional>
#include <string>
// 模拟从数据库查询用户信息的结构体
struct User {
int id;
std::string firstName;
std::optional<std::string> middleName;
std::string lastName;
};
// 模拟从数据库查询用户信息的函数
std::optional<User> queryUserFromDatabase(int userId) {
// 模拟数据库查询逻辑
if (userId == 1) {
return User{1, "John", "Paul", "Doe"};
} else if (userId == 2) {
return User{2, "Jane", std::nullopt, "Smith"};
} else {
return std::nullopt; // 用户不存在
}
}
void printUserInfo(const User& user) {
std::cout << "User ID: " << user.id << std::endl;
std::cout << "First Name: " << user.firstName << std::endl;
if (user.middleName) {
std::cout << "Middle Name: " << *user.middleName << std::endl;
} else {
std::cout << "Middle Name: Not provided" << std::endl;
}
std::cout << "Last Name: " << user.lastName << std::endl;
}
int main() {
int userId = 1;
auto user = queryUserFromDatabase(userId);
if (user) {
printUserInfo(*user);
} else {
std::cout << "User with ID " << userId << " not found." << std::endl;
}
userId = 2;
user = queryUserFromDatabase(userId);
if (user) {
printUserInfo(*user);
} else {
std::cout << "User with ID " << userId << " not found." << std::endl;
}
userId = 3;
user = queryUserFromDatabase(userId);
if (user) {
printUserInfo(*user);
} else {
std::cout << "User with ID " << userId << " not found." << std::endl;
}
return 0;
}
- 错误处理:虽然
std::optional本身并不直接用于错误处理(通常推荐使用异常或返回状态码和输出参数的方式),但在某些场景下,它可以作为返回值的一部分,用于表示某个操作可能产生的额外可选信息或结果。
比如下面的例子,parseInt用于将字符串转换成整型数字。std::stoi在转换失败后会抛出异常,而我们又不希望parseInt的使用者关心这个异常。那么我们就可以捕获异常,然后通过std::nullopt表达转换失败。
#include <iostream>
#include <optional>
#include <string>
// 函数尝试将字符串转换为整数
std::optional<int> parseInt(const std::string& str) {
try {
size_t pos;
int value = std::stoi(str, &pos);
if (pos == str.length()) {
return value;
} else {
return std::nullopt; // 字符串包含非数字字符
}
} catch (const std::invalid_argument&) {
return std::nullopt; // 字符串不是有效的整数
} catch (const std::out_of_range&) {
return std::nullopt; // 整数超出范围
}
}
int main() {
std::string input1 = "123";
std::string input2 = "abc";
std::string input3 = "123abc";
std::string input4 = "999999999999999999999999999999";
auto result1 = parseInt(input1);
auto result2 = parseInt(input2);
auto result3 = parseInt(input3);
auto result4 = parseInt(input4);
if (result1) {
std::cout << "Parsed value: " << *result1 << std::endl;
} else {
std::cout << "Failed to parse: " << input1 << std::endl;
}
if (result2) {
std::cout << "Parsed value: " << *result2 << std::endl;
} else {
std::cout << "Failed to parse: " << input2 << std::endl;
}
if (result3) {
std::cout << "Parsed value: " << *result3 << std::endl;
} else {
std::cout << "Failed to parse: " << input3 << std::endl;
}
if (result4) {
std::cout << "Parsed value: " << *result4 << std::endl;
} else {
std::cout << "Failed to parse: " << input4 << std::endl;
}
return 0;
}
代码
https://github.com/f304646673/cpulsplus/tree/master/optional
209

被折叠的 条评论
为什么被折叠?



