这篇文章介绍下C++17
引入的std::optional
为什么要有 optional
一般来说,如果想要一个函数返回“多个”值,C++程序员倾向于使用结构体/类完成这个操作。即定义一个通用的结构体,在函数内部完成装填,然后返回一个实例化的结构体。
#include <iostream>
using namespace std;
struct Out {
string out1 { "" };
string out2 { "" };
};
pair<bool, Out> func(const string& in) {
Out o;
if (in.size() == 0)
return { false, o };
o.out1 = "hello";
o.out2 = "world";
return { true, o };
}
int main() {
if (auto [status, o] = func("hi"); status) {
cout << o.out1 << endl;
cout << o.out2 << endl;
}
return 0;
}
(代码来自 https://zhuanlan.zhihu.com/p/64985296)
C++17引入了std::optional,介绍如下:
The class template std::optional manages an optional contained value,
i.e. a value that may or may not be present. A common use case for
optional is the return value of a function that may fail. As opposed
to other approaches, such as std::pair<T,bool>, optional handles
expensive-to-construct objects well and is more readable, as the
intent is expressed explicitly. 类模板 std::optional
管理一个可选的容纳值,即可以存在也可以不存在的值。 一种常见的 optional 使用情况是一个可能失败的函数的返回值。与其他手段,如
std::pair<T,bool> 相比, optional 良好地处理构造开销高昂的对象,并更加可读,因为它显式表达意图。
换句话说,一个普通的变量,只有不同的值。而optional相当于在外面套了一层壳,先考虑存不存在值,再考虑值是多少。
使用
对于像我这样的菜鸟来说,optional 的源代码十分复杂的。但是使用起来并不困难,理解上面的含义就行。
/**
* @brief Class template for optional values.
*/
template<typename _Tp>
class optional
: private _Optional_base<_Tp>,
private _Enable_copy_move<
// Copy constructor.
is_copy_constructible_v<_Tp>,
// Copy assignment.
__and_v<is_copy_constructible<_Tp>, is_copy_assignable<_Tp>>,
// Move constructor.
is_move_constructible_v<_Tp>,
// Move assignment.
__and_v<is_move_constructible<_Tp>, is_move_assignable<_Tp>>,
// Unique tag type.
可以看到,本质上其实是个模板类,将包裹的类型以模板参数T的方式传入。
初始化
optional几种初始化如下:
//初始化为空
optional<int> int_empty;
//使用有效值初始化(C++其他初始化方法也同样可用)
optional<int> my_int = 2;
//使用make_optional
auto doubleOpt = std::make_optional(10.0);
auto complexOpt = std::make_optional<std::complex<double>>(3.0, 4.0);
//使用in_place
optional<vector<int>> vectorOpt{in_place, {1, 2, 3}};
//使用其它optional对象复制/移动构造
auto optCopied = vectorOpt;
说一下第三点和第四点,就是这个std::make_optional
和std::in_place
。我搜到的资料显示这代表直接调用模板T的构造函数,即“完美转发”。
假设我有这么一个类 TestOp:
struct TestOp
{
TestOp() : age(100) {}
int getAge() { return age; }
private:
int age;
};
它有一个无参数的构造函数,但这个构造函数为成员赋默认值。这时,若使用如下写法
optional<TestOp> test_op;
cout << test_op.value().getAge() << endl;
会编译失败。因为optional并不会调用TestOp的默认构造函数,只会创建一个空的TestOp对象。
这让我非常不理解,因为就算是未传入参数,应该调用T的默认构造函数才对。
你可能想到,如果初始化时直接传入构造函数呢?
optional<TestOp> test_op{TestOp()};
这样写当然没问题,我们将得到包含默认TestOp
对象的optional对象。但是在上面的代码中,将先构造出一个TestOp的临时对象,然后调用move函数将这个临时对象“移动”到optional存储的对象中,带来了额外的开销。在这种情况下,我们就只能使用std::in_place
/ std::make_optional
来“原地”构造optional底层存储的对象。
另一种情况是,TestOp禁用了移动构造和复制构造函数,这时候也只能用std::in_place
/ std::make_optional
。
最后一种必须使用std::in_place
/ std::make_optional
的情况是,当你需要使用多个参数初始化同一个变量时,比如std::vector
。没错,原生的 optional
构造函数,并不支持多参数,神奇吧?
我猜想多参数的支持会导致歧义。