【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::optionalor_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;
}
posted @ 2025-08-01 09:27  仰望星河Leon  阅读(19)  评论(0)    收藏  举报