在这里插入图片描述

📃个人主页:island1314

⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞

  • 生活总是不会一帆风顺,前进的道路也不会永远一马平川,如何面对挫折影响人生走向 – 《人民日报》


一、 std::optional

std::optional 是 C++17 引入的一个模板类,它用于表示一个可能包含值或者不包含值(即“无值”状态)的容器。

为什么需要 std::optional?

在程序设计中,我们经常会遇到一些场景,某个变量可能有值也可能没有值,通常就会用如下方式处理 “可能无返回值” 情况:

  • 返回特殊值:例如 find 函数返回 -1 或 string::npos,返回 nullptr 指针等。问题在于:这些特殊值没有类型安全保证,调用者很容易忘记检查,代码可读性差。
  • 使用输出参数:通过引用传递参数来存储结果,函数本身返回一个bool 表示成功与否。语法笨拙,不够直观。
  • 抛出异常:并非所有“无结果”的情况都是异常,有时它只是一个正常的、可预期的分支。使用异常来处理控制流可能开销较大且不直观。

但这些方法存在一定的缺陷,比如空指针解引用会导致程序崩溃,标记值可能和正常值冲突等。而 std::optional 提供了一种更安全、更明确的方式来处理这种“可选值”场景,它将值(或有或无)包装在⼀个类型中,强制调用者处理可能无值的情况

常用接口

特性 说明 代码示例
创建空 表示无值 std::optionalempty;
auto empty = std::nullopt;
创建有值 包装一个值 std::optionalopt = 5;
auto opt = std::make_optional(5);
检查 判断是否包含值 if(opt.has_value()){…}
if(opt){…}
安全取值 有值返回值,无值抛异常 int x = opt.value();
安全取值(带默认) 无值时返回默认值 int x = opt.value_or(0);
不安全取值 必须确保有值,否则 UB int x = *opt;
重置 使其变为空 opt.reset();
opt=std::nullopt;

std::optional 的主要特性

  1. 表示有值或无值状态std::optional 可以明确地表示一个值是否存在。通过成员函数 has_value() 可以检查是否有值,返回 true 表示有值,false 表示无值。

  2. 安全地访问值:如果 std::optional 对象有值,可以通过 value() 成员函数访问它;如果无值,调用 value() 会抛出异常 std::bad_optional_access。此外,还可以使用 operator*operator-> 来访问值,这与智能指针的用法类似。

  3. 避免空指针问题:与使用裸指针表示无值不同,std::optional 不会因为解引用空指针而导致程序崩溃。它将无值状态封装在类型系统中,使得对值的访问必须先进行检查。

示例

#include <iostream>
#include <vector>
#include <map>
#include <optional>

// 结合文⽂档,optional的基本使用
void test_example1(){
	// 1、定义optional 对象 
	std::optional<int> maybeInt; // 初始为空 
	std::optional<std::string> maybeString = "Hello"; // 初始有值 
	std::optional<double> empty = std::nullopt; // 显式设置为空 
	std::optional<double> opt = std::make_optional(3.14); // 使用 std::make_optional 创建对象

	// 2、检查是否有值 
	if (maybeInt.has_value()) {
		std::cout << "has_value1: " << *maybeInt << std::endl;
	}
	// 或者更简洁的写法 
	if (maybeString) {
		std::cout << "has_value2: " << *maybeString << std::endl;
	}

	// 3、尝试访问无值的 optional
	try {
		int value = maybeInt.value();
	}catch (const std::bad_optional_access& e) {
		std::cout << e.what() << std::endl; // 无值时抛出 std::bad_optional_access 
	}
	
	// 安全取值
	maybeInt = 1;
	std::cout << "maybeInt has value: " << maybeInt.value_or(-1) << std::endl;

	// 不安全但快速的访问 - 无值时行为未定义 
	int value1 = *maybeInt;

	// 带默认值的访问 
	int value2 = maybeInt.value_or(2); // ⽆值时返回2 
	std::cout << "value2: " << value2 << std::endl;

	// 4、修改值 
	maybeInt = 42; // 赋新值 
	maybeInt = std::nullopt; // 设为空 
	maybeInt.reset(); // 设为空 
}

// optional实践中的使用场景 
void test_example2(){
	// 查找⼀个顶点对应的下标编号 
	std::map<std::string, int> indexMap = { {"张庄",1}, {"王村",2}, {"李家村",3}, {"王家坪",3} };
	auto findIndex = [&indexMap](const std::string& str)->std::optional<int>
	{
		auto it = indexMap.find(str);
		if (it != indexMap.end()){
			return it->second;
		}else{
			return std::nullopt;
		}
	};
	std::string x;
	std::cin >> x;
	std::optional<int> index = findIndex(x);
	if (index){
		std::cout << x << "对应的编号为:" << *index << std::endl;
	}else{
		std::cout << x << "是非法顶点" << std::endl;
	}
	std::vector<std::string> v = { "张庄", "李庄", "" };
	auto access = [&v](int i)-> std::optional<std::string>
	{
		if (i < v.size()){
			return v[i];
		}else{
			// 以前的语法这里函数只能⽤抛异常或者断⾔⽅式处理i越界的情况 
			// i越界不能返回"",因为正常数据可能就是"",⽆法区分 
			std::nullopt;
		}
	};
}
	
int main(){
	test_example1();
	test_example2();
	return 0;
}

使用场景

  1. 函数返回值:当函数可能成功返回一个值,也可能不返回任何值时,std::optional 是一个很好的选择。例如,从集合中查找一个元素,如果找到就返回该元素,否则返回无值。
  2. 配置参数:在某些配置场景中,某些参数可能是可选的。使用 std::optional 可以清晰地表示这些参数是否有值。
  3. 避免特殊标记值:当不想使用特殊的标记值(如 -1、空字符串等)来表示无值状态时,std::optional 提供了更语义化的方式。

注意事项

  1. 类型限制std::optional 的模板参数类型不能是不完全类型(incomplete type),并且不能是 std::in_place_t 类型。

  2. 性能开销:虽然 std::optional 提供了安全性,但它可能会带来一些性能开销,因为它需要存储额外的状态信息(表示是否有值)。在性能敏感的场景中,需要权衡其使用是否合适。

  3. 与指针的区别:std::optional 和指针都可以表示无值状态,但 std::optional 将值存储在其内部,而指针指向外部存储。对于小对象或频繁使用的值,std::optional 可能更高效;对于大对象或需要动态分配的情况,指针可能更合适。

二、 std::variant

1. 基本概述

std::variant 是 C++17 引入的一种类型安全的联合体(Union),它允许可变类型的数据存储在一个统一的类型中。

  • 传统联合体(union)的问题:C风格或 C++的普通 union 不是类型安全的。
    • 我们需要自己记住当前存储的是哪种类型,如果访问错了(比如在一个存储 int 的 union 上读取 float),会导致未定义行为,而且它无法处理非平凡类型(如 std::string)
  • std::variant 的优势是它解决了所有这些问题。它自己知道当前存储的是哪种类型,并确保对象被正确构造和析构,而且能够有效防止非法类型转换和访问越界等问题,可以把它想象成一个 “智能的” “类型丰富的union

核心特性

  • 类型安全:通过编译期检查,确保对 std::variant 中存储的数据的访问是合法的,防止类型越界等问题。
  • 可变类型存储:支持在一个变量中存储多种不同类型的值。
  • 访问控制:提供多种访问方式,如 std::getstd::visit 等,确保安全地访问存储的值。

使用场景

  • 类型不确定的场景:当函数返回值或数据类型不确定时,std::variant 可以提供统一的接口。
  • 可选值场景:在需要表示无值或多种可能值的情况下,std::variant 是一个很好的选择。
  • 类型安全的联合体:传统联合体类型不安全,容易导致类型越界等问题,std::variant 提供了更安全的替代方案。
2. 定义和访问
#include <iostream>
#include <variant>
#include <string>

int main() {
	std::variant<int, double, std::string> v;

	v = 42;				// 现在持有 int
	std::cout << "int: " << std::get<int>(v) << std::endl;

	v = 3.14;			// 现在持有 double
	std::cout << "double: " << std::get<double>(v) << std::endl;

	v = "hello";		// 现在持有 string
	std::cout << "string: " << std::get<std::string>(v) << std::endl;

	// 赋值时找不到对应类型的值则报错
	// v = std::pair<int, int>{};	// Error

	// 使用 index() 获取当前持有的类型索引
	std::cout << "Current Index: " << v.index() << std::endl;
	return 0;
}

注意std::variant 是一个类型安全的联合体,它要求其模板参数中的类型是唯一的。如果试图在 std::variant 中存储两个相同类型的值,编译器会报错,因为这违反了 std::variant 的设计原则。

std::variant<std::string,std::string> v2;
v2 = "abc"; // Error

解决:使用 emplace

int main() {
	std::variant<std::string, std::string> v2;
	v2.emplace<1>("ghi");
	// v2.emplace<std::string>("abc");	// Error
	std::cout << std::get<1>(v2) << "\n";

	std::variant<std::string> v3;
	v3.emplace<0>("abc");
	std::cout << std::get<0>(v3) << "\n";
	v3.emplace<std::string>("abc");	
	std::cout << std::get<0>(v3) << "\n";

	return 0;
}
3. 访问值

方式一:使用 std:get<Type>std:.get<index> 可以通过类型或索引来直接获取值

  • 但如果当前variant 存储的不是你请求的类型/索引,它会抛出 std::bad_variant_access 异常。
int main() {
    std::variant<int, double> v = 42;
    try {
        std::cout << std::get<int>(v) << std::endl;
        std::cout << std::get<0>(v) << std::endl;
        std::cout << std::get<double>(v) << std::endl; // 抛出异常 
    }catch (const std::bad_variant_access& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
}

方式二:使用 std:get_if<Type> std::get_if 不会抛出异常

  • 它接受一个指针参数,如果 variant 当前存储的是指定类型,则返回一个指向该值的指针;否则返回 nullptr
int main() {
    std::variant<int, double, std::string> v = "hello";
    // 使⽤std::get_if尝试获取值 
    if (auto pval = std::get_if<int>(&v)) {
    	std::cout << "int value: " << *pval << std::endl;
    }else if (auto pval = std::get_if<double>(&v)) {
    	std::cout << "double value: " << *pval << std::endl;
    }else if (auto pval = std::get_if<std::string>(&v)) {
   		std::cout << "string value: " << *pval << std::endl;
    }
}

方式三:使用 std::visit(推荐,最安全强大)std::visit 允许你提供一个“访问者”(visitor)来根据当前存储的类型执行相应的操作,这是最类型安全、最清晰的方式。

语法

template <class Visitor, class... Variants>
constexpr auto visit(Visitor&& vis, Variants&&... vars);
  • Visitor:访问者类型,通常是一个重载了 operator() 的类或 lambda 表达式。
  • Variants:一个或多个 std::variant 对象。

示例

#include <iostream>
#include <variant>
#include <string>

// 定义一个访问者类
struct Visitor {
    // 重载 operator() 对 int 类型
    void operator()(int value) const {
        std::cout << "Integer: " << value << ", Size: " << sizeof(value) << " bytes" << std::endl;
    }
    // 重载 operator() 对 double 类型
    void operator()(double value) const {
        std::cout << "Double: " << value << ", Size: " << sizeof(value) << " bytes" << std::endl;
    }
    // 重载 operator() 对 std::string 类型
    void operator()(const std::string& value) const {
        std::cout << "String: " << value << ", Length: " << value.length() << std::endl;
    }
};

int main() {
    // 创建一个 std::variant 对象
    std::variant<int, double, std::string> v;

    // 赋值为 int 类型
    v = 42;
    // 使用 std::visit 和自定义访问者类
    std::visit(Visitor{}, v);

    // 赋值为 double 类型
    v = 3.14;
    std::visit(Visitor{}, v);

    // 赋值为 std::string 类型
    v = std::string("Hello, World!"); // 使用 std::string 构造函数
    std::visit(Visitor{}, v);

    // 使用 lambda 表达式作为访问者
    std::visit([](auto&& arg) {
        if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, int>) {
            std::cout << "Lambda - Integer: " << arg << ", Size: " << sizeof(arg) << " bytes" << std::endl;
        }else if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, double>) {
            std::cout << "Lambda - Double: " << arg << ", Size: " << sizeof(arg) << " bytes" << std::endl;
        }else if constexpr (std::is_same_v<std::decay_t<decltype(arg)>, std::string>) {
            std::cout << "Lambda - String: " << arg << ", Length: " << arg.length() << std::endl;
        }
    }, v);

    // 对多个 std::variant 进行操作
    std::variant<int, double> v1 = 100;
    std::variant<int, double> v2 = 200.5;

    std::visit([](const auto& a, const auto& b) {
        std::cout << "Sum: " << a + b << std::endl;
        }, v1, v2);

    return 0;
}

注意事项

  • 访问者必须覆盖所有类型:访问者需要为 std::variant 中定义的所有类型提供对应的 operator() 重载,否则会导致编译错误。
  • 性能开销std::visit 的调用涉及函数指针调用或模板实例化,可能会带来一定的性能开销。在性能敏感的场景中需要权衡使用。
  • 编译器支持:确保你的编译器支持 C++17 或更高版本。

示例:实战实现哈希桶

#include <iostream>
#include <variant>
#include <vector>
#include <list>
#include <set>

template<class ...Ts>
struct overloaded : Ts ...{using Ts::operator()...; };

template<class ...Ts>
overloaded(Ts ...)->overloaded<Ts ...>;

template<class K, size_t Len = 8>
class HashTable {
	using Value = std::variant<std::list<K>, std::set<K>>;
public:
	HashTable(): _t(10){}

	void Insert(const K& key) {
		size_t index = key % _t.size();

		// 链表插入
		auto listInsert = [this, &key, index](std::list<K>& lt) {
			if (lt.size() < Len) lt.push_back(key);
			else {
				// 大于 8 转换插入到红黑树 set
				std::set<K> s(lt.begin(), lt.end());
				s.insert(key);
				_t[index] = move(s);
			}
		};
		// 红黑树插入
		auto setInsert = [&key](std::set<K>& s) {
			s.insert(key);
		};
		std::visit(overloaded{ listInsert, setInsert }, _t[index]);
	}

	bool Find(const K& key){
		size_t index = key % _t.size();

		// 链表查找 
		auto listFind = [&key](std::list<K>& lt) ->bool{
				return std::find(lt.begin(), lt.end(), key) != lt.end();
		};
		// 红黑树查找 
		auto setFind = [&key](std::set<K>& s) ->bool{
				return s.count(key);
		};
		return std::visit(overloaded{listFind,setFind}, _t[index]);
	}

private:
	std::vector<Value> _t;
};

int main(){
	HashTable<int> ht;
	for (size_t i = 0; i < 10; i++){
		ht.Insert(i * 10 + 5);
	}
	std::cout << ht.Find(15) << std::endl;
	std::cout << ht.Find(3) << std::endl;
	return 0;
}

三、std::any

1. 基本概述

std::any 是 C++17 引入的一个类型安全的通用容器,可以存储 任何类型(必须是可拷贝构造的) 单个值的容器

  • 当我们从 any 中取出值时,我们须知道其 原始类型,并通过 std::any_cast 进行安全的转换。如果类型不匹配,它会 抛异常返回空指针

  • 与原始的 void* 不同,any 会记录类型信息,并在 any_cast 时进行检查。其次它管理着自己内部存储的对象生命周期(构造、拷贝、析构),为了避免为小对象频繁分配堆内存,许多实现会使用一个 小缓冲区优化(SB0,Small BufferOptimization),MSVC下的std:string 就使用了这个优化。

  • 它类似于 std::variant,但与 std::variant 不同的是,std::variant 在编译期就确定了可以存储的类型列表,而 std::any 可以存储任意类型的数据。

核心特性

  • 存储任意类型:可以存储任何类型的数据,包括内置类型和自定义类型。
  • 类型安全:提供类型检查机制,确保对存储数据的访问是合法的。
  • 动态类型查询:可以通过 type() 成员函数查询存储的数据类型。
  • 值访问:通过 any_cast 安全地访问存储的值。

使用场景

  • 存储未知类型的数据:在需要存储类型不确定的数据时,std::any 提供了一个通用的解决方案。
  • 函数返回值:当函数可能返回多种不同类型的值时,std::any 可以提供一个统一的返回类型。
  • 配置参数:在配置文件或参数系统中,std::any 可以用于存储各种类型的配置值。

std::any 的接口非常简洁,主要包含以下成员函数:

函数 作用 示例代码
构造函数 创建一个 std::any 对象,可以初始化为任意类型的值。 std::any any_value = 42;
std::any any_value = std::string("Hello");
operator= 给 std::any 对象赋值为任意类型的值。 any_value = 3.14;
any_value = std::vector<int>{1, 2, 3};
emplace(args…) 原地构造一个类型为 T 的对象,参数 args 传递给 T 的构造函数。 any_value.emplace<std::string>("World");
any_value.emplace<int>(42);
reset() 销毁内部包含的对象,使 std::any 变为空。 any_value.reset();
has_value() 返回一个 bool,判断 std::any 对象当前是否包含一个值。 if (any_value.has_value()) { ... }
type() 返回一个 std::type_info const&,表示当前包含值的类型。如果 std::any 为空,则返回 typeid(void)。 std::cout << any_value.type().name();
std::any_cast 用于从 std::any 对象中提取值。如果转换失败,会抛出 std::bad_any_cast 异常。 int value = std::any_cast<int>(any_value);
std::string str = std::any_cast<std::string>(any_value);
2. 使用

示例一:定义和访问

#include <iostream>
#include <any>
#include <string>
#include <typeindex>
#include <typeinfo>

int main() {
    // 1. 创建一个 std::any 对象
    std::any any1 = 42;     // 存储 int 
    std::any any2 = 3.14;   // 存储 double 
    std::any any3 = std::string("Hello, World!");   // 存储 string 
    std::any any4 = std::pair<std::string, std::string>("xxx", "yyy"); // 存储 pair

    // 2. 获取类型信息
    std::cout << "any1: " << std::any_cast<int>(any1) << ", Type: " << typeid(any1).name() << std::endl; //  class std::any
    const std::type_info& ti = any1.type();
    std::cout << "any1: " << std::any_cast<int>(any1) << ", Type: " << ti.name() << std::endl;  // int
    

    // 3. 检查类型并访问值
    if (any3.type() == typeid(std::string)) {
        std::string value = std::any_cast<std::string>(any3);
        std::cout << "any3 cast to std::string: " << value << std::endl; // Hello, World!
    }else {
        std::cerr << "Type mismatch!" << std::endl;
    }

    // 4. 尝试访问错误的类型
    try {
        int value = std::any_cast<int>(any3);
    }catch (const std::bad_any_cast& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }

    // 5. 使用 emplace 原地构造
    any3.emplace<std::string>("world");

    // 6. 检查是否有值
    if (any3.has_value()) {
        std::cout << "a has value" << std::endl;
    }
    return 0;
}

还记得上面我说过,他这里用到了优化,看看如下代码就知道了

std::cout << sizeof(any1) << "\n";  // 64
std::cout << sizeof(any2) << "\n";  // 64

然后调试可以看到其底层的数据结构

image-20251110165935743

示例二:取值

#include <iostream>
#include <any>
#include <string>
#include <vector>
#include <cassert>

// vector中存储any类型 
void anyVector() {
    std::string str("hello world");
    std::vector<std::any> v = { 1.1, 2, str };
    for (const auto& item : v) {
        if (item.type() == typeid(int)) {
            std::cout << "整数配置: " << std::any_cast<int>(item) << '\n';
        }
        else if (item.type() == typeid(double)) {
            std::cout << "浮点配置: " << std::any_cast<double>(item) << '\n';
        }
        else if (item.type() == typeid(std::string)) {
            // 移除 const 属性以支持引用访问
            std::cout << "字符串配置: " << std::any_cast<std::string&>(const_cast<std::any&>(item)) << '\n';
        }
        else {
            assert(false);
        }
    }
}

int main() {
    std::any a1 = 42;                    // 存放 int 
    std::any a2 = 3.14;                  // 存放 double 
    std::any a3 = std::string("Hello");   // 存放 std::string 

    // 方式一:转换为值的类型(如果类型不匹配,抛出 std::bad_any_cast) 
    try {
        int int_value = std::any_cast<int>(a1); // 正确,a1 存放的是 int 
        std::cout << "Value: " << int_value << '\n';
        double double_value = std::any_cast<double>(a1); // 错误!抛出异常 
    }catch (const std::bad_any_cast& e) {
        std::cout << "Cast failed: " << e.what() << '\n';
    }

    // 方式二: 转换为值和转换为值的引用
    // 这里 any_cast 返回的是 a3 存储对象的拷贝,要尽量避免这样使用
    std::string str_ref1 = std::any_cast<std::string>(a3);
    str_ref1[0]++;
    std::cout << str_ref1 << '\n'; // 输出 "Iello"

    std::string& str_ref2 = std::any_cast<std::string&>(const_cast<std::any&>(a3)); 
    str_ref2[0]++;  // 对原值产生影响
    std::cout << std::any_cast<std::string&>(const_cast<std::any&>(a3)) << '\n'; // 输出 "Iello"

    // 方式三: 转换为指针(如果类型不匹配,返回 nullptr,不会抛异常) 
    if (auto ptr = std::any_cast<int>(&a1)) { // 传递指针,返回指针 
        std::cout << "Value via pointer: " << *ptr << '\n';
    }else {
        std::cout << "Not an int or is empty.\n";
    }

    anyVector();

    return 0;
}

注意事项

  • 类型检查:访问 std::any 中的值时,必须进行类型检查,否则会引发 std::bad_any_cast 异常。
  • 性能开销std::any 的类型检查和动态类型信息存储会带来一定的性能开销。
  • std::variant 的区别std::variant 在编译期确定可以存储的类型列表,而 std::any 可以存储任意类型;std::variant 更适合存储有限的几种类型,std::any 更适合存储未知或动态类型。
3. 对比 variant
特性 std::any std::variant
类型安全性 运行时检查类型,any_cast 失败抛出异常。 编译期检查类型,不匹配直接编译错误。
存储类型 可以存储任意类型。 必须在模板参数中显式指定所有可能的类型。
类型查询 提供 type() 方法查询当前存储类型的 std::type_info 不直接提供类型查询方法,类型检查在编译期完成。
性能开销 由于动态类型信息,可能稍高。 更高效,编译期确定类型,无运行时类型存储开销。
适用场景 适用于需要存储未知类型或动态类型的场景。 适用于已知可能类型的有限集合,类型安全更重要。
异常处理 any_cast 失败时抛出 std::bad_any_cast 访问不存在的类型时抛出 std::bad_variant_access
存储类型数量 无限制。 模板参数中类型数量有限(通常不超过 30 个)。
类型擦除 是,将具体类型擦除为通用 std::any 否,类型在编译期明确。

★,°:.☆( ̄▽ ̄)/$:.°★ 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【C++】的内容,请持续关注我 !!

在这里插入图片描述

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐