C++07_std::tuple、std::optional、std::variant、std::visit
std::tuple
std::tuple
常用容器:tuple
std::tuple<...> 可以将多个不同类型的值打包成一个。尖括号里填各个元素的类型。之后可以用 std::get<0> 获取第0个元素,std::get<1> 获取第1个元素,以此类推(从0开始数数)。
#include <iostream>
#include <tuple>
int main() {
auto tup = std::tuple<int, float, char>(3, 3.14f, 'h');
int first = std::get<0>(tup);
float second = std::get<1>(tup);
char third = std::get<2>(tup);
std::cout << first << std::endl;
std::cout << second << std::endl;
std::cout << third << std::endl;
return 0;
}
如何化简
当用于构造函数时,std::tuple<...> 尖括号里的类型可以省略,这是 C++17 的新特性:CTAD。
通过 auto 自动推导 get 的返回类型。
int main() {
auto tup = std::tuple(3, 3.14f, 'h');
auto first = std::get<0>(tup);
auto second = std::get<1>(tup);
auto third = std::get<2>(tup);
return 0;
}
结构化绑定
可是需要一个个去 get 还是好麻烦。没关系,可以用结构化绑定的语法:auto [x, y, ...] = tup;
利用一个方括号,里面是变量名列表,即可解包一个 tuple。里面的数据会按顺序赋值给每个变量,非常方便。
int main() {
auto tup = std::tuple(3, 3.14f, 'h');
auto [first, second, third] = tup;
std::cout << first << std::endl;
std::cout << second << std::endl;
std::cout << third << std::endl;
return 0;
}
结构化绑定为引用
结构化绑定也支持绑定为引用:auto &[x, y, ...] = tup;
这样相当于解包出来的 x, y, ... 都是 auto & 推导出来的引用类型。对引用的修改可以影响到原 tuple 内的值。
同理,通过 auto const & 绑定为常引用:auto const &[x, y, ...] = tup;
常引用虽然不能修改,但是可以避免一次不必要拷贝。
结构化绑定:还可以是任意自定义类
其实,结构化绑定不仅可以解包 std::tuple,还可以解包任意用户自定义类:
struct MyClass {
int x;
float y;
};
int main() {
MyClass mc = {42, 3.14f};
auto [x, y] = mc;
std::cout << x << ", " << y << std::endl;
return 0;
}
配合打包的 {} 初始化表达式,真是太便利了!惊不惊喜?意不意外?可惜 std::get 并不支持自定义类。
用于函数多个返回值
std::tuple 可以用于有多个返回值的函数。
如上一讲中所说,当函数返回值确定时,return 可以用 {} 表达式初始化,不必重复写前面的类名 std::tuple。
#include <iostream>
#include <tuple>
#include <cmath>
std::tuple<bool, float> mysqrt(float x) {
if (x >= 0.f) {
return {true, std::sqrt(x)};
} else {
return {false, 0.0f};
}
}
int main() {
auto [success, value] = mysqrt(3.f);
if (success) {
std::cout << "success! result: " << value << std::endl;
} else {
std::cout << "failure! result: " << value << std::endl;
}
return 0;
}
std::optional
std::optional
常用容器:optional
有些函数,本来要返回 T 类型,但是有可能会失败!
上个例子中用 std::tuple<bool, T>,其中第一个 bool 表示成功与否。但是这样尽管失败了还是需要指定一个值 0.0f,非常麻烦。
这种情况推荐用 std::optional
成功时,直接返回 T。失败时,只需返回 std::nullopt 即可。
#include <iostream>
#include <cmath>
#include <optional>
std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}
int main() {
auto ret = mysqrt(3.f);
if (ret.has_value()) {
std::cout << "success! result: " << value << std::endl;
} else {
std::cout << "failure! result: " << value << std::endl;
}
return 0;
}
value_or() 方便地指定一个缺省值
ret.value_or(3)
等价于:ret.has_value() ? ret.value() : 3
value() 会检测是否为空,空则抛出异常
当 ret 没有值时(即 nullopt),ret.value() 会抛出一个异常,类型为 std::bad_optional_access
。
std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}
int main() {
auto ret = mysqrt(-3.14f);
std::cout << "success! result: " << ret.value() << std::endl;
return 0;
}
operator*() 不检测是否为空,不会抛出异常
除了 ret.value() 之外还可以用 *ret 获取 optional 容器中的值,不过他不会去检测是否 has_value(),也不会抛出异常,更加高效,但是要注意安全。
请确保在 has_value() 的分支内使用 *ret,否则就是不安全的。
如果 optional 里的类型是结构体,则也可以用 ret->xxx 来访问该结构体的属性。
std::optional<float> mysqrt(float x) {
if (x >= 0.f) {
return std::sqrt(x);
} else {
return std::nullopt;
}
}
int main() {
auto ret = mysqrt(3.14f);
if (ret.has_value()) {
std::cout << "success! result: " << *ret << std::endl;
} else {
std::cout << "failure!" << std::endl;
}
return 0;
}
operator bool() 和 has_value() 等价
在 if 的条件表达式中,其实可以直接写 if (ret),他和 if (ret.has_value()) 等价。
没错,这样看来 optional 是在模仿指针,nullopt 则模仿 nullptr。但是他更安全,且符合 RAII 思想,当设为 nullopt 时会自动释放内部的对象。
利用这一点可以实现 RAII 容器的提前释放。和 unique_ptr 的区别在于他的对象存储在栈上,效率更高。
int main() {
auto ret = mysqrt(3.14f);
if (ret) {
std::cout << "success! result: " << *ret << std::endl;
} else {
std::cout << "failure!" << std::endl;
}
return 0;
}
std::variant、std::visit
std::variant、std::visit
variant:安全的 union,存储多个不同类型的值
有时候需要一个类型“要么存储 int,要么存储 float”,这时候就可以用 std::variant<int, float>。
和 union 相比,variant 符合 RAII 思想,更加安全易用。
给 variant 赋值只需用普通的 = 即可。
variant 的特点是只存储其中一种类型。
tuple 的特点是每个类型都有存储。
请区分,根据实际情况选用适当的容器。
#include <variant>
int main() {
std::variant<int ,float> v = 3;
v = 3.14f;
return 0;
}
获取容器中的数据用 std::get
要获取某个类型的值,比如要获取 int 用 std::getstd::bad_variant_access
。
此外,还可以通过 std::get<0> 获取 variant 列表中第 0 个类型,这个例子中和 std::get
int main() {
std::variant<int ,float> v = 3;
std::cout << std::get<int>(v) << std::endl;
std::cout << std::get<0>(v) << std::endl;
v = 3.14f;
std::cout << std::get<float>(v) << std::endl;
std::cout << std::get<1>(v) << std::endl;
return 0;
}
判断当前是哪个类型用 std::holds_alternative
可以用 std::holds_alternative
void print(std::variant<int, float> const& v) {
if (std::holds_alternative<int>(v)) {
std::cout << std::get<int>(v) << std::endl;
} else if (std::holds_alternative<float>(v)) {
std::cout << std::get<float>(v) << std::endl;
}
}
int main() {
std::variant<int ,float> v = 3;
print(v);
v = 3.14f;
print(v);
return 0;
}
判断当前是哪个类型用 v.index()
除了这个之外,还可以用成员方法 index() 获取当前是参数列表中的第几个类型。这样也可以实现判断。
void print(std::variant<int, float> const& v) {
if (v.index() == 0) {
std::cout << std::get<int>(v) << std::endl;
} else if (v.index() == 1) {
std::cout << std::get<float>(v) << std::endl;
}
}
批量匹配 std::visit
如果你的 if-else 每个分支长得都差不多(除了 std::get<> 的类型不一样以外),可以考虑用 std::visit,他会自动用相应的类型,调用你的 lambda,lambda 中往往是个重载函数。
这里用到了带 auto 的 lambda,利用了他具有多次编译的特性,实现编译多个分支的效果。
std::visit、std::variant 的这种模式称为静态多态,和虚函数、抽象类的动态多态相对。
静态多态的优点是:性能开销小,存储大小固定。缺点是:类型固定,不能运行时扩充。
void print(std::variant<int, float> const& v) {
std::visit([&] (auto const& t) {
std::cout << t << std::endl;
}, v);
}
std::visit:还支持多个参数
其实还可以有多个 variant 作为参数。
相应地 lambda 的参数数量要与之匹配。
std::visit 会自动罗列出所有的排列组合!
所以如果 variant 有 n 个类型,那 lambda 就要被编译 n² 次,编译可能会变慢。
但是标准库能保证运行时是 O(1) 的(他们用函数指针实现分支,不是暴力 if-else)。
void print(std::variant<int, float> const& v) {
std::visit([&] (auto const& t) {
std::cout << t << std::endl;
}, v);
}
auto add(std::variant<int, float> const& v1, std::variant<int, float> const& v2) {
std::variant<int, float> ret;
std::visit([&](auto const& t1, auto const& t2) {
ret = t1 + t2;
}, v1, v2);
return ret;
}
int main() {
std::variant<int ,float> v = 3;
print(add(v, 3.14f));
return 0;
}
std::visit:可以有返回值
std::visit里面的 lambda 可以有返回值,不过都得同样类型。
利用这一点进一步优化:
auto add(std::variant<int, float> const& v1, std::variant<int, float> const& v2) {
return std::visit([&](auto const& t1, auto const& t2) -> std::variant<int, float> {
return t1 + t2;
}, v1, v2);
}
Reference: