在实际开发中,我们经常会遇到这样的需求:
一个函数有时能返回有效数据,有时返回失败。
那么,函数该怎么告诉调用者“我失败了”?

以前我们可能会这样写:


传统写法一:返回空字符串

#include <iostream>
#include <fstream>

std::string ReadFileAsString(const std::string& filePath)
{
	std::ifstream stream(filePath);
	// 输入文件流
	// 如果文件打开成功 又或者无法打开 要处理它
	if (stream)
	{
		std::string result; // 用于存储从文件中读取的内容
		// read_file
		stream.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_file
		stream.close();
		outSuccess = true; // 表示读取成功
		return result;
	}
	
	outSuccess = false; // 表示读取失败
	return std::string();
}

调用方式:

int main()
{
    bool fileOpenSuccessfully;
    std::string data = ReadFileAsString("data.txt", fileOpenSuccessfully);
    if (fileOpenSuccessfully)
    {
        std::cout << "文件读取成功!内容如下:\n" << data << std::endl;
    }
    else
    {
        std::cout << "文件读取失败!" << std::endl;
    }
}

这种方式虽然能区分成功与失败,但函数签名变得复杂,需要额外的布尔参数,而且语义也不够清晰。


使用 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 不能直接当作字符串使用,它只是一个包装容器。
只有在解包后(用 *datadata.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 让代码表达力更强,逻辑更清晰。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐