【c++】std::tuple/std::variant/std::any/std::optional/std::expected<T,E>
1. std::tuple
https://blog.csdn.net/haokan123456789/article/details/136006995
C++11引入了元组tuple。
类似结构体,存储多个不同类型的数据,数据个数同模板参数个数相同,且数据类型同模板参数对应。
使用场景:传参或返回值时,需要一次性传递多个数据时。
#include<tuple>
template<class... Types>
class Tuple;
(1)初始化
std::tuple<int,std::string,std::vector<int>> tpe1(1, "hello", std::vector<int>{1,2,3,4,5});
std::tuple<int,std::string,std::vector<int>> tpe2{1, "hello", std::vector<int>{1,2,3,4,5}};
auto tpe3 = make_tuple(1, "hello", std::vector<int>{1,2,3,4,5});
std::tuple<int,std::string,std::vector<int>> tpe4 = tpe3;//若数据类型都支持拷贝,则tuple可拷贝
(2)访问
get<i> 要求 i 是编译期常量。传入get<index>的index需在编译器确定,不可在运行时传入,否则报错。
若想index传入遍历,需用模板参数。
//取值
auto a = std::get<0>(tpe1);
cout<<a<<endl;//1
//修改
auto b = std::get<2>(tpe1);
for(auto v:b)
{
cout<<v<<endl;
}
std::get<0>(tpe1) = 10; //1修改为10
//解包取值
bool myBool;
int myInt;
double myDouble;
std::string mystring;
std::tie(myBool, myInt, myDouble, mystring)= std::make_tuple(true, 1,3.0, "1112222");
//若需忽略某个元素,可通过std::ignore
bool myBool;
std::string mystring;
std::tie(myBool, std::ignore, std::ignore, mystring)= std::make_tuple(true, 1,3.0, "1112222");
(3)遍历
使用模板参数包展开
//错误!
//tup是运行时变量,模板参数必须是类型或编译期常量
template<class Tuple , std::size_t size>
class PrintTuple
{
public:
void printTuple(const Tuple tup)
{
PrintTuple<tup,size - 1>::printTuple(tup);
cout << std::get<size - 1>(tup) << endl;
}
};
template<class Tuple , std::size_t size>
class PrintTuple
{
public:
static void printTuple(const Tuple tup)
{
PrintTuple<Tuple,size - 1>::printTuple(tup);
cout << std::get<size - 1>(tup) << endl;
}
};
template<class Tuple>
class PrintTuple<Tuple, 1>
{
public:
static void printTuple(const Tuple tup)
{
cout << std::get<0>(tup) << endl;
}
};
// 封装PrintTuple,限制PrintTuple的类型为std::tuple,同时通过参数包获取tuple的size
template<class... Args>
void TuplePrint(const std::tuple<Args...>& tup)
{
PrintTuple<decltype(tup), sizeof...(Args)>::printTuple(tup);
}
int main()
{
tuple<string, vector<int>> tup{ "hello", vector<int>{1,2,3,4,5} };
PrintTuple<decltype(tup), 2>::printTuple(tup);
TuplePrint(tup);
}
(3.1)std::apply
std::apply 是 C++17 引入的一个非常有用的工具,用于将第二个参数的内容展开为第一个参数即指定函数的形参列表,并调用指定函数。
std::apply 让你像调用普通函数一样调用 tuple 的内容,是写泛型代码、反射、序列化等场景的神器。
-
第一个参数:
函数
lambda
成员函数指针
函数对象
等任何可调用对象。 -
第二个参数:
std::tuple
std::pair
std::array
任何支持 std::get(t) 的类型。 -
普通函数
可以使用std::apply,std::apply可以推导普通函数f的类型
#include <iostream>
#include <tuple>
using namespace std;
void print(int a, double b, const string& c) {
cout << a << ", " << b << ", " << c << endl;
}
int main() {
tuple<int, double, string> t{42, 3.14, "hello"};
apply(print, t); // 等价于 print(42, 3.14, "hello");
}
- 函数模板
std::apply无法推到模板函数的类型,因此,需改用泛型lambda
template<typename T>
T void func(T t1, T t2)
{
return t1+t2;
}
//会报错,std::apply无法推到func类型
std::apply(func, std::make_pair(1.0f,2.0f));
- 泛型lambda
auto lmb = [](auto a, auto b){return a+b;}
auto lmb2 = [](auto&... args){} //形参列表是参数包
std::apply(lmb,make_pair(1.0f,2.0f));
- 成员函数
#include <iostream>
#include <tuple>
using namespace std;
struct Person {
void say(string msg, int times) {
for (int i = 0; i < times; ++i)
cout << msg << endl;
}
};
int main() {
Person p;
tuple<string, int> args{"Hi!", 3};
apply(&Person::say, p, args); // 等价于 p.say("Hi!", 3);
}
- 获取返回值
#include <iostream>
#include <tuple>
using namespace std;
int add(int a, int b) { return a + b; }
int main() {
tuple<int, int> t{3, 4};
int result = apply(add, t);
cout << "sum = " << result << endl;
}
(3.2)使用std::apply遍历tuple
template <class F, class Tuple>
constexpr decltype(auto) std::apply(F&& f, Tuple&& t);
//f:可调用的函数、lambda、成员函数等
//t:要展开的 std::tuple
//返回值:函数 f 的返回值
//lambda+参数包展开处理tuple
#include <iostream>
#include <tuple>
using namespace std;
int main()
{
tuple<string, int, double> tup{"Alice", 25, 1.68};
//t中参数传给args,args通过逗号表达式展开
std::apply([](const auto&... args) {
((cout << args << endl), ...);
}, tup);
}
重载<<操作符:
template<typename... Args>
std::ostream& operator<<(std::ostream& os, const tuple<Args...> tup)
{
std::apply([&os](Args... args) {
((cout << args << endl), ...);
}, tup);
return os;
}
int main()
{
tuple<string, vector<int>> tup{ "hello", vector<int>{1,2,3,4,5} };
cout << tup;
}
(4)获取tuple的size
2. std::variant
std::variant 是 C++17 引入的类型安全的联合体(sum type),可以在编译期指定的一组类型中存储任一值,并自带类型检查和访问机制。存储类型列表中任意一种可能的类型。
#include <variant>
(1)空构造
默认构造为模板参数列表 第一个类型 的默认值。
std::variant 没有空状态(不像 std::optional的std::nullopt),但可用 std::monostate 占位。
std::monostate 是 C++17 引入的一个 空占位类型,专为配合 std::variant 使用而设计,核心用途是:
当 variant 的第一个类型不可默认构造时,把 std::monostate 放在类型列表首位,表示当前variant没有设置有效值,“无值”或“占位”状态;
否则 variant 会默认构造为第一个类型的默认值。
std::variant<int,string> w; //w存储的是int默认值0
std::variant<std::monostate, int, double> w; // w默认值
//等价于
std::variant<std::monostate, int, double> w{ std::monostate{} };
//判断是否设置了有效值
if (std::holds_alternative<std::monostate>(value))
{
std::cout << "value 是空的\n";
}
value = 42;
if (!std::holds_alternative<std::monostate>(value))
{
std::cout << "value 现在有值了\n";
}
(2)访问
-
std::get
(v):若指定的类型T错误,抛出异常: std::bad_variant_access。 -
使用类型访问
std::variant<int, double, std::string> v;
v = 42; // 存 int
std::cout << std::get<int>(v) << '\n'; // 42
v = "hello"; // 改为 string
std::cout << std::get<std::string>(v) << '\n'; // hello
- 使用index访问
这里的index是实际存储类型在模板参数中的index
int main()
{
std::variant<int, double, std::string> v;
v = 42; // 存 int
std::cout << std::get<0>(v) << '\n'; // 42
v = "hello";
std::cout << std::get<2>(v) << '\n'; // get<0>会抛异常:std::bad_variant_access
}
- 安全访问
std::get_if 的安全在于 “失败不抛异常,只返回空指针”,让你用简单的 if (ptr) 就能写出健壮、无异常的访问逻辑
std::variant<int, std::string> v = 42;
if (int* p = std::get_if<int>(&v))
{
std::cout << "int: " << *p << '\n';
}
else
{
std::cout << "not an int\n";
}
(3)元素的类型
v.index() 返回的是 std::size_t 类型的索引值
3. std::any
std::any 是 C++17 引入的一个类型安全的、可以存储任意类型值的容器。
#include<any>
| 函数名 | 说明 |
|---|---|
a.has_value() |
判断是否有值 |
a.reset() |
清空内容 |
a.type() |
返回 std::type_info const& |
std::any_cast<T>(a) |
获取值,类型不对会抛 std::bad_any_cast |
std::make_any<T>(...) |
构造一个 std::any 对象 |
//std::any_cast<T>(a) 就是从 std::any 中按类型取出值,可以理解拆箱转成具体类型,用错类型会抛异常或返回空指针
std::any a = 42; // 存的是 int
int value = std::any_cast<int>(a); // 正确:取出 int
std::string s = std::any_cast<std::string>(a); // 错误:类型不对,抛出异常
std::any a = 42;
// 1. 拷贝值(可能抛异常)
int x = std::any_cast<int>(a);
// 2. 获取引用(可以修改原值)
int& ref = std::any_cast<int&>(a);
ref = 100; // 修改后 a 里的值也变成 100
// 3. 获取指针(不抛异常,失败返回 nullptr)
int* ptr = std::any_cast<int>(&a);
if (ptr) {
std::cout << *ptr << std::endl;
}
4. std::optional
- std::optional 是 C++17 引入的一个模板类,用于表示一个可能不存在的值。它位于头文件
中,是 C++ 标准库中用于处理“可选值”的推荐方式。 - 要么有值,要么为空。
- 可链式调用。
#include<optional>
(4.1)构造
std::optional<int> o1; // 空,默认构造,等价于std::nullopt
std::optional<int> o4{std::nullopt}; // 空
std::optional<int> o2 = 42; // 有值
std::optional<int> o3{42};
(4.2)判空
if (o2) { std::cout << *o2; } // 解引用
if (o2.has_value()) { ... } // 同上
(4.3)取值
int v1 = o2.value(); // 空则抛 std::bad_optional_access
int v2 = o2.value_or(-1); // 空时返回 -1
int v3 = *o2; // UB 若空
(4.4)链式调用API
- func都必须要有返回值
transform
- func函数体必须有返回值,返回普通类型(“裸类型”T)。
T func(){}
std::optional<T> res = opt.transform(func);
- opt为空,不会调用函数,直接返回空
std::optional<T>{std::nullopt}。 - opt非空,对opt应用一个函数,函数return普通类型T,返回std::optional
。 - transform 不会因为你提供的函数返回了某种“空”值(比如 0、空字符串、nullptr 等)就变成空 optional。它只看原 optional 是否为空。
- 处理结果不会返回空
std::nullopt。
std::optional<int> opt{42};
std::optional<string> res = opt.transform([](int x){return to_string(x);});
and_then
- func函数体必须有返回值,返回
std::optional<T>,而不能返回“裸类型”T。
std::optional<T> func(){}
std::optional<T> res = opt.and_then(func);
- opt为空,不会调用函数,直接返回空。
- opt非空,对opt调用函数,函数必须return optional
类型。 - 函数体可以返回空,返回的空会继续传递下去。
std::optional<int> o{42};
std::optional<string> res = opt.and_then([](int x){
if(x<0)
return std::optional<int>{std::nullopt};
else
return std::optional<int>{x};
});
or_else
- opt非空,则直接返回opt。
- opt为空
std::nullopt,则调用函数func并返回。 - func返回值必须为std::optional
。 - lambda只能是无形参列表的。(std::expceted<T,E>的
or_else可以带形参)
因为 std::nullopt 没有任何附加信息,所以 lambda 无法拿到“错误内容”。
std::optional<int> opt;
std::optional<int> result = opt.or_else([] { return std::optional<int>(0); });
(4.6)链式调用
- C++23起,加入std::optional 的链式调用接口(transform / and_then / or_else)到标准库。
- 使用场景:只要你在写“如果每一步都成功就继续,否则提前返回空/默认值”的逻辑,链式调用就能让你的代码从“嵌套判空地狱”变成“一条声明式流水线”。
- 失败即短路:只要链式调用中的某个 and_then 返回 std::nullopt,链路即停止,后续的所有 and_then / transform 都会被跳过,直接跳到最后的 or_else,执行其中的回调并返回它的结果。
- 写法:
保持整条链每一步(包括or_else)的返回类型始终是 std::optional,并且在最后统一通过 or_else处理“失败”。
func执行结果可能为空的,也就是说包含边界条件提前return的,用and_then。
执行结果不会为空的,也就是说边界条件全部剔除后,最后的处理,用transform
最后处理空,也就是统一再次处理边界条件,用or_else
maybeA
.and_then(step1) // 若 step1 返回 nullopt → 链结束,result 为 nullopt
.transform(step2) // 上一步已是 nullopt,step2 不会被执行
.or_else(fallback) // 只有整条链最终为 nullopt 时,才执行 fallback
auto tmp = maybeA.and_then(step1).transform(step2);
// tmp 的类型是 std::optional<U>
auto result = tmp.or_else(fallback);
如果 step1 返回 nullopt,tmp 就是 nullopt → 执行 fallback。
如果 step1 返回一个值,但 step2 返回 nullopt,tmp 仍是 nullopt → 也执行 fallback。
如果 step1 和 step2 都返回非空,tmp 非空 → 不会执行 fallback。
//每一层都要显式 if (!x) return nullopt;
//调用端还得二次判空、解引用。
std::optional<int> ageFromJson(const nlohmann::json& j)
{
if (!j.contains("user")) return std::nullopt;
const auto& user = j["user"];
if (!user.contains("profile")) return std::nullopt;
const auto& profile = user["profile"];
if (!profile.contains("age")) return std::nullopt;
if (!profile["age"].is_number()) return std::nullopt;
int age = profile["age"];
if (age < 0) return std::nullopt;
return age;
}
int main()
{
nlohmann::json doc = /* ... */;
auto ageOpt = ageFromJson(doc);
if (!ageOpt)
{
std::cout << "age not found\n";
}
else
{
int nextAge = *ageOpt + 1;
std::cout << "next age: " << nextAge << '\n';
}
}
//没有显式 if/return nullopt,每一步“失败即短路”。
//调用端拿到一个 std::optional<int>,要么有值要么已给出默认,无需二次判空。
auto nextAge = nlohmann::json{/* … */}
.and_then([](const auto& j) -> std::optional<const nlohmann::json&> {
return j.contains("user") ? std::optional{j["user"]} : std::nullopt;
})
.and_then([](const auto& user) -> std::optional<const nlohmann::json&> {
return user.contains("profile") ? std::optional{user["profile"]} : std::nullopt;
})
.and_then([](const auto& profile) -> std::optional<int> {
if (!profile.contains("age") || !profile["age"].is_number()) return std::nullopt;
int age = profile["age"];
return age >= 0 ? std::optional{age} : std::optional{std::nullopt};
})
.transform([](int age) { return age + 1; })
.or_else([] {
std::cout << "age not found\n";
return std::optional<int>{-1}; // 给调用端一个默认
});
(4.7)处理异常
- 把异常语义转换成“有值 / 无值”语义——成功就返回含值的 optional,任何失败都返回 nullopt。
- 异常时无法存储std::nullopt以外的信息,若需要存储字符串等异常信息,可用C++23起的
std::expected<T,E> - 这样调用者就不需要 try-catch,而是像普通可选值一样使用 and_then / transform / or_else 进行链式处理。
std::optional<int> parse_int(std::string_view s) noexcept
{
try {
size_t pos = 0;
int v = std::stoi(std::string(s), &pos);
if (pos != s.size()) return std::nullopt; // 非全部数字
return v;
} catch (...) {
return std::nullopt; // stoi 抛异常 -> 失败,只返回空
}
}
5. std::expected<T,E>
C++23起提供std::expected<T,E>
提供链式调用,可以像用 std::optional 那样写出链式调用——任何一步失败都会自动短路,把错误一路带到链尾。
#include <expected>
- transform:在“成功”分支上继续操作,用法同
optional::transform。函数return“裸类型”,返回值类型为std::expected<T,E>
std::expected<int, std::string> e = 42;
auto doubled = e.transform([](int v){ return v * 2; });
// doubled 类型 expected<int,std::string>
- and_then:在“成功”分支上继续操作,用法同
optional::transform。函数return:正确值——std::expected<int,std::string>,异常值——std::unexpected("failed")
std::expected<int, std::string> safe_sqrt(int x)
{
return x>=0 ? std::expected<int,std::string>{static_cast<int>(std::sqrt(x))}
: std::unexpected("failed");
}
auto res = e.and_then(safe_sqrt);
-
transform_error:在“错误”分支上继续操作
-
or_else:在“错误”分支上继续操作
lambda可以带形参,因为可以带错误信息。std::optional上or_else不可带形参。
std::expected<int, std::string> e = std::unexpected("file not found");
auto r = e.or_else([](const std::string& err) { // 可以带参
std::cout << "error: " << err << '\n';
return std::expected<int, std::string>{0};
});
#include <expected>
#include <iostream>
#include <string>
#include <cmath>
// 可能失败的几个小函数
std::expected<int, std::string> parse(std::string_view s)
{
try {
size_t pos = 0;
int v = std::stoi(std::string(s), &pos);
if (pos != s.size()) return std::unexpected("trailing chars");
return v;
} catch (...) {
return std::unexpected("not an int");
}
}
std::expected<int, std::string> reciprocal(int x)
{
return x != 0 ? std::expected<int, std::string>{1 / x}
: std::unexpected("division by zero");
}
int main()
{
auto result = parse("42")
.and_then(reciprocal) // 成功值继续
.transform([](int v){ return v * 100; })
.or_else([](const std::string& e){
std::cout << "Handled: " << e << '\n';
return 0; // 兜底值
});
std::cout << *result << '\n'; // 输出 2
}
6. optional和expected区别
optional侧重于表示一个值的可选性,它只关心这个值是否存在,不涉及错误处理的概念。本质上还是类似tuple/variant这种容器,只是可以表示空状态。
而expected则用于处理操作的成功或失败,当操作失败时,它能提供具体的错误信息。expected类型是用于在函数返回时,表示操作可能成功也可能失败的情况。它就像一个“带错误信息的optional”。当函数调用成功时,它包含一个预期的结果值;当函数调用失败时,它包含一个错误码或错误对象。
比如,假设有一个函数,它可能返回一个整数,也可能不返回任何值:
std::optional<int> find_value_in_vector(const std::vector<int>& vec, int target)
{
for (auto value : vec) {
if (value == target) {
return value;
}
}
return std::nullopt;
}

浙公网安备 33010602011771号