本文记录C++17新特性之std::any和string_view.
文章目录
- [第2章 C++17标准库特性](#第2章 C++17标准库特性)
-
- [2.3 std::any](#2.3 std::any)
-
- [2.3.1 常用方法](#2.3.1 常用方法)
- [2.3.2 使用举例](#2.3.2 使用举例)
- [2.3.3 any 和 variant 对比](#2.3.3 any 和 variant 对比)
- [2.4 std::string_view](#2.4 std::string_view)
-
- [2.4.1 实现原理](#2.4.1 实现原理)
- [2.4.2 使用示例](#2.4.2 使用示例)
第2章 C++17标准库特性
2.3 std::any
在C++11和C++14中,如果我们想在同一个变量中存储不同类型的值,可以使用如下方法:
方式1,void* 指针:这是 C 语言时代的解决方案。虽然灵活,但它完全丧失了类型信息,极其不安全,且无法自动管理内存(需要手动转换和释放)。
方式2,union:只能存储预定义的几种类型,且对于非平凡类型(non-trivial types,如 std::string)的支持非常有限且复杂。
C++17中 std::any 的引入旨在提供一种类型安全、拥有值语义且无需模板参数即可持有任意类型对象的标准解决方案。
2.3.1 常用方法
1 has_value() : 返回一个bool值,用于检查std::any对象当前是否包含一个值。
2 type():返回一个const std::type_info & ,代表当所存储对象的类型。结合typeid()判断要处理的类型。
3 std::any_cast(): 有两种使用方式
值版本: std::any_cast(a)。如果类型 T 与 a 中存储的类型匹配,它会返回该值的拷贝或引用。如果类型不匹配,它会抛出 std::bad_any_cast 异常。
指针版本: std::any_cast(&a)。这个版本更安全,因为它不会抛出异常。如果类型匹配,它返回一个指向所存储对象的指针;如果不匹配,则返回 nullptr。
4 reset(): 销毁 std::any 中存储的对象,使其变为空。
2.3.2 使用举例
使用举例:测试上面的any的成员方法。
cpp
void test()
{
std::any a = 42; // 存储一个整数
cout << "存储的整数: " << std::any_cast<int>(a) << endl;
// 存储的整数: 42
std::cout << "Has value? " << a.has_value() << std::endl;
// Has value? 1
// 存储string类型
a = std::string("hello world");
if (a.type() == typeid (std::string))
{
std::cout << "存储的字符串: " << std::any_cast<std::string>(a) << std::endl;
}
else if (a.type() == typeid(int))
{
std::cout << "存储的整数: " << std::any_cast<int>(a) << std::endl;
}
// 存储的字符串: hello world
// 重置 reset
a.reset();
// 测试转换
std::any a2 = 3.14;
try
{
// 正确转换
double val = std::any_cast<double>(a2);
std::cout << "Value: " << val << std::endl;
// Value: 3.14
// 错误转换,抛出异常
int i = std::any_cast<int>(a2);
}
catch (const std::bad_any_cast& e)
{
std::cerr << "Cast error: " << e.what() << std::endl;
// Cast error: Bad any_cast
}
// 使用指针转换版本,不抛出异常。
if (double* ptr = std::any_cast<double>(&a2))
{
cout << "Pointer cast value: " << *ptr << endl;
}
else
{
cout << "Pointer cast failed." << endl;
}
// Pointer cast value: 3.14
}
示例2:std::any在容器中的应用,any存储不同的类型,然后使用any_cast<>转换并获取对应类型的数据。
cpp
struct Point
{
int x;
int y;
};
void test()
{
// 容器存储不同类型的值
std::vector<std::any> vec;
vec.push_back(33);
vec.emplace_back(std::string("Hello"));
vec.emplace_back(Point{ 10,20 });
// 访问
for (const auto& item : vec)
{
if (item.type() == typeid(int))
{
std::cout << "整数: " << std::any_cast<int>(item) << std::endl;
}
else if (item.type() == typeid(std::string))
{
std::cout << "字符串: " << std::any_cast<std::string>(item) << std::endl;
}
else if (item.type() == typeid(Point))
{
Point p = std::any_cast<Point>(item);
std::cout << "Point: (" << p.x << ", " << p.y << ")" << std::endl;
}
}
/*
整数: 33
字符串: Hello
Point: (10, 20)
*/
}
2.3.3 any 和 variant 对比
1 any和variant都是类型安全,自动管理生命周期,支持任意可拷贝构造的类型。
2 性能开销比variant大,类型转换必须精确匹配。
3 如果预先知道类型是固定的,优先使用std::variant;如果未知,可使用std::any.
2.4 std::string_view
C++17之前我们在编写带有字符串参数的函数时,参数类型通常使用 const std::string & 常量引用,直接访问原始string对象,避免拷贝传入的string。但是缺点是,如果传入的是C风格的字符串,比如"hello",编译器必须隐士创建一个临时的std::string对象,然后再传入引用,这就造成了性能开销。
C++17中使用std::string_view 解决上面传递临时对象开辟内存的问题。
2.4.1 实现原理
std::string_view包含在头文件<string_view>中,实现原理:
1 指针 (ptr):指向字符串数据的起始位置(可以是 std::string 的 buffer,也可以是 C 风格字符串字面量,或者是 vector 的 buffer)。
2 长度 (size):记录字符串的长度。
实现源码大概如下:
cpp
// 伪代码
template <class CharT>
class basic_string_view {
const CharT* data_;
size_t size_;
// ... 提供了类似 std::string 的只读接口
};
2.4.2 使用示例
示例1:替代const std::string & 作为参数
cpp
// 传统写法:传入 "Hello" 时会产生临时 std::string 对象及堆分配
void printStringOld(const std::string& str)
{
std::cout << str << std::endl;
}
// C++17 写法:无论传 std::string 还是 "Hello",都没有堆分配
void printStringNew(std::string_view sv)
{
std::cout << sv << std::endl;
}
void test()
{
const char* cstring = "c风格字符串";
std::string cppstring = "C++字符串";
// 1. 接受 C 风格字符串 (零开销)
printStringNew(cstring);
// c风格字符串
printStringNew("Literal String");
// Literal String
// 2. 接受 std::string (零开销,自动隐式转换)
printStringNew(cppstring);
// C++字符串
}
示例2:高效的子串操作
string的对象调用substr时会创建一个新的字符串,而使用std::string_view 调用substr仅仅调整内部的指针和长度,时间复杂度O(1),零拷贝。
cpp
void test()
{
std::string_view sv = "Hello, std::string_view!";
std::string_view sub = sv.substr(1, 5);
cout << "子串: " << sub << endl;
// 子串: ello,
}
示例3:生命周期管理注意
由于string_view不拥有数据,只是对原始数据的操作,所以必须确保原始数据有效。
cpp
// 错误: temp字符串在函数结束时被销毁,返回的string_view悬空
std::string_view getBadView()
{
string temp = "临时字符串";
return std::string_view(temp); //
}
void test()
{
std::string_view ret = getBadView();
std::cout << "悬空视图内容: " << ret << std::endl;
// 悬空视图内容: 烫烫?
std::string str = "Hello";
std::string_view sv = str;
str += " World"; // str 可能重新分配内存
std::cout << "视图内容: " << sv << std::endl;
// 视图内容: Hello
// sv 仍指向旧内存,str内部的buffer已经更新,sv内容未变
}
使用总结:
1 使用时,直接传递string_view,不用传递 string_view 引用。
2 之后使用substr时,使用string_view的substr,避免拷贝,提高性能。