C++17 新特性:std::optional —— 优雅处理函数返回值
在实际开发中,我们经常会遇到这样的需求:
一个函数有时能返回有效数据,有时返回失败。
那么,函数该怎么告诉调用者“我失败了”?
以前我们可能会这样写:
传统写法一:返回空字符串
#include <iostream>
#include <fstream>std::string ReadFileAsString(const std::string& filePath)
{std::ifstream stream(filePath);// 输入文件流// 如果文件打开成功 又或者无法打开 要处理它if (stream){std::string result; // 用于存储从文件中读取的内容// read_filestream.close();return result;}// 如果不成功return std::string();// 返回空字符串对象 利用std::string的默认构造函数 等价于std::string("")
}
然后在调用时:
int main()
{std::string data = ReadFileAsString("data.txt");if (data != "")// 但是假如文件就在那里 它是空的 但它是有效的 我们需要一种方式确定它是否有效{// 文件读取成功}
问题在于:
如果文件本身就是空的,我们就无法区分“文件是空的”和“文件没打开成功”。
传统写法二:用引用参数输出成功标志
#include <iostream>
#include <fstream>std::string ReadFileAsString(const std::string& filePath, bool& outSuccess)
{std::ifstream stream(filePath);if (stream){std::string result;// read_filestream.close();outSuccess = true; // 表示读取成功return result;}outSuccess = false; // 表示读取失败return std::string();
}
调用方式:
int main()
{bool fileOpenSuccessfully;std::string data = ReadFileAsString("data.txt");if (fileOpenSuccessfully){//}
这种方式虽然能区分成功与失败,但函数签名变得复杂,需要额外的布尔参数,而且语义也不够清晰。
使用 std::optional 改进
C++17 引入了 std::optional,用来表示“一个可能存在、也可能不存在的值”。
它就像一个可空容器,里面可能装着一个 T 类型的值,也可能什么都没有。
基本示例
#include <iostream>
#include <fstream>
#include <optional>std::optional<std::string> ReadFileAsString(const std::string& filePath)
{std::ifstream stream(filePath);if (stream){std::string result;// read_file...stream.close();return result; // 有值的情况}return {}; // 无值
}int main()
{std::optional<std::string> data = ReadFileAsString("data.txt");if (data.has_value()) // 或者直接 if (data){std::cout << "File read successfully!\n";// 取出其中的值std::string& str = *data; // 解包操作// 或者// std::string str = data.value();}else{std::cout << "File could not be opened!\n";}
}
std::optional 的常用操作
std::optional<T>
:定义一个可能包含 T 类型数据的对象return {}
:返回“没有值”.has_value()
:判断是否包含有效值operator bool()
:可直接写 if (opt)*opt
:解包取出值.value()
:获取值(若无值会抛异常).value_or(default)
:获取值或默认值
使用 std::optional 取出值
当我们调用 ReadFileAsString
并得到一个 std::optional<std::string>
时,注意:
data
不是字符串本身,而是一个“可能装着字符串”的容器。
std::optional<std::string> data = ReadFileAsString("data.txt");
此时有两种情况:
文件读取成功 →
data
里装着一个std::string
文件读取失败 →
data
是空的,没有任何值
解包 optional 中的值
如果我们确定 data
里有值(比如在 if (data)
判断之后),
就可以取出内部的字符串对象,方法有两种:
std::string& str = *data;
或
std::string str = data.value();
这里的 *data
并不是“指针解引用”,
而是 optional 类型的“解包”操作 —— 取出它内部存放的那个实际对象。
也就是说:
data
是一个std::optional<std::string>
*data
是其中真正的std::string
因此,data
不能直接当作字符串使用,它只是一个包装容器。
只有在解包后(用 *data
或 data.value()
)才能得到真正的字符串对象。
示例:
if (data)
{std::cout << "File content:\n";std::cout << *data; // 解包后打印
}
else
{std::cout << "File could not be opened.\n";
}
示例:value_or
std::optional<std::string> data = ReadFileAsString("data.txt");// 如果 data 有值,返回其内容;
// 否则返回 "File not found"
std::string content = data.value_or("File not found");
std::cout << content << std::endl;
示例:基本类型
std::optional<int> count;
int c = count.value_or(100); // 如果 count 没值,默认返回 100
关于文件路径
文件路径是相对于项目的“工作目录”的。
如果你的 data.txt 文件放在项目根目录(即 .vcxproj 文件所在目录),可以直接:
ReadFileAsString("data.txt");
如果放在 src/ 目录下,则应写:
ReadFileAsString("src/data.txt");
优点总结
使用 std::optional 具有以下优点:
语义清晰,能明确表达“值是否存在”
函数接口简洁,无需额外的输出参数
使用安全,可以用 value_or 提供默认值
与现代 C++ 风格一致,易于维护
总结
std::optional<T> 是一个模板类,用来表达“值可能存在,也可能不存在”。
它可以替代传统的“返回空对象”或“额外的 bool 参数”。
optional 并不是指针,但你可以通过 *data 或 data.value() 访问内部对象。
当值不存在时,可以使用 .value_or(default) 提供安全的默认返回。
当一个函数的返回值“不一定有意义”时,用 std::optional 让代码表达力更强,逻辑更清晰。