C++(七):std::any+std::string_view

1:开篇

上一篇我们学习了编译期类型范围确定的 std::optionalstd::variant,它们在类型已知的场景下提供了完善的类型安全保障。本篇我们补充两个定位不同的标准库组件:

  1. std::any :运行时的通用类型容器,可以存储任意可拷贝类型,替代不安全的 void*,自带类型检查与生命周期管理
  2. std::string_view:零拷贝字符串视图,以非拥有的方式观察字符串,彻底消除传参、解析场景下的冗余字符串拷贝

两个特性都直击传统 C++ 开发的痛点:一个解决通用类型存储的安全性问题,一个解决字符串处理的性能问题,学习成本低,收益非常显著。

2:std::any运行时类型安全的通用容器

1:历史痛点

在需要存储 "任意类型" 的场景(如配置系统、消息队列、异构容器),传统 C++ 通常使用 void* 来实现,但它存在无法忽视的致命问题:

  1. 完全无类型安全:取出值时完全依赖开发者手动记忆原始类型,类型转换错误直接导致未定义行为,崩溃且难以排查
  2. 生命周期完全手动管理void* 只保存地址,不会调用构造和析构函数,存储非平凡类型(如 string、vector)时必须手动管理内存,极易泄漏
  3. 代码可读性差:业务逻辑中充斥着强制类型转换,维护成本极高
cpp 复制代码
// 传统void*写法
void* data = new std::string("hello");
// ...
std::string* str = static_cast<std::string*>(data); // 类型全靠记,错了就崩
// 忘记delete就内存泄漏

2:核心接口与基本用法

std::any 是一个可以存储任意可拷贝构造类型的单值容器,自带类型信息,支持安全的类型转换,自动管理对象生命周期。

核心接口

接口 作用
默认构造 / 拷贝构造 / 赋值 支持直接用任意可拷贝类型赋值
emplace<T>(args...) 原地构造 T 类型对象,避免额外拷贝
reset() 销毁内部对象,变为空状态
has_value() 判断是否包含有效值
type() 返回当前存储值的类型信息(std::type_info
std::any_cast<T>() 类型安全的取值转换,失败抛异常或返回空指针

基础实例

cpp 复制代码
#include <any>
#include <string>
#include <iostream>

int main() {
    // 1. 构造与赋值
    std::any a1 = 42;                  // 存储int
    std::any a2 = 3.14;                // 存储double
    std::any a3 = std::string("Hello"); // 存储std::string
    std::any a4;                        // 空状态

    a4 = std::make_pair(std::string("key"), std::string("value"));

    // 2. 原地构造,避免临时对象拷贝
    a3.emplace<std::string>("World");

    // 3. 状态与类型检查
    if (a3.has_value()) {
        std::cout << "a3 has value" << std::endl;
        const std::type_info& ti = a1.type();
        std::cout << "a1 type: " << ti.name() << std::endl;
    }

    // 4. 重置为空
    a1.reset();
    return 0;
}

三种any_cast取值方式

std::any_cast 是访问 std::any 的唯一途径,有三种使用形式,对应不同场景:

cpp 复制代码
#include <any>
#include <string>
#include <iostream>

int main() {
    std::any a = std::string("Hello");

    // 方式1:值拷贝,类型不匹配抛 std::bad_any_cast 异常
    try {
        std::string s = std::any_cast<std::string>(a); // 正确
        std::cout << "Value: " << s << '\n';
        // double d = std::any_cast<double>(a); // 错误,抛出异常
    } catch (const std::bad_any_cast& e) {
        std::cout << "Cast failed: " << e.what() << '\n';
    }

    // 方式2:引用形式,避免拷贝,可修改内部值
    std::string& s_ref = std::any_cast<std::string&>(a);
    s_ref[0] = 'h';
    std::cout << std::any_cast<const std::string&>(a) << '\n'; // 输出 hello

    // 方式3:指针形式,传入any的指针,不匹配返回nullptr,不抛异常
    if (auto* p = std::any_cast<int>(&a)) {
        std::cout << "int value: " << *p << '\n';
    } else {
        std::cout << "Not an int" << '\n';
    }
    return 0;
}

3:底层原理

std::any 的核心设计是带类型信息的类型擦除容器 ,主流实现都会加入**小缓冲区优化(Small Buffer Optimization, SBO)**来减少堆分配:

存储结构:内部包含三部分核心信息

  • 类型信息指针:指向 std::type_info 运行时类型信息,用于类型检查
  • 存储缓冲区:小对象直接存在栈上的内部缓冲区(通常几个指针大小),大对象则分配堆内存,存储堆指针
  • 操作函数表:保存拷贝、析构等操作的函数指针,实现类型擦除后的生命周期管理
cpp 复制代码
// 简化版底层结构
class any {
private:
    const std::type_info* type_; // 类型信息
    union {
        std::byte small_buffer_[sizeof(void*) * 3]; // 小对象缓冲区
        void* heap_ptr_; // 大对象堆指针
    } storage_;
    // 操作函数指针:拷贝、析构等
    void (*destroy_)(void*);
    void* (*clone_)(const void*);
};
  • 小缓冲区优化 (SBO)
    • 小对象(大小不超过内部缓冲区、对齐要求满足)直接在栈上构造,无堆分配开销
    • 大对象在堆上分配,内部存储指针
    • 类似 std::string 的 SSO 短字符串优化,目的都是减少内存分配次数
  • 类型擦除机制:通过函数指针封装不同类型的操作,对外只暴露统一接口,实现 "存储任意类型" 的能力;同时保留类型信息,保证转换时的安全检查
  • 生命周期自动管理:赋值、析构时自动调用对应类型的构造 / 析构函数,无需手动干预

4:实战场景

1:异构配置容器

存储不同类型的配置项,替代 void* 或复杂的继承体系:

cpp 复制代码
#include <any>
#include <string>
#include <vector>
#include <iostream>
#include <cassert>

void printConfig(const std::vector<std::any>& configs) {
    for (const auto& item : configs) {
        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)) {
            std::cout << "字符串配置: " << std::any_cast<const std::string&>(item) << '\n';
        } else {
            assert(false && "未知配置类型");
        }
    }
}
2:哈希桶的any版本实现
cpp 复制代码
#include <any>
#include <list>
#include <set>
#include <vector>
#include <algorithm>
#include <cassert>

template<class K, size_t Len = 8>
class HashTable {
public:
    HashTable() : _tables(10, std::list<K>()) {}

    void Insert(const K& key) {
        size_t hashi = key % _tables.size();

        auto listInsert = [this, &key, hashi](std::list<K>& lt) {
            if (lt.size() < Len) {
                lt.push_back(key);
            } else {
                std::set<K> s(lt.begin(), lt.end());
                s.insert(key);
                _tables[hashi] = std::move(s);
            }
        };

        auto setInsert = [&key](std::set<K>& s) {
            s.insert(key);
        };

        // 手动尝试类型转换,判断当前存储的类型
        if (auto* ptr = std::any_cast<std::list<K>>(&_tables[hashi])) {
            listInsert(*ptr);
        } else if (auto* ptr = std::any_cast<std::set<K>>(&_tables[hashi])) {
            setInsert(*ptr);
        } else {
            assert(false);
        }
    }

    bool Find(const K& key) {
        size_t hashi = key % _tables.size();
        if (!_tables[hashi].has_value()) return false;

        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);
        };

        if (auto* ptr = std::any_cast<std::list<K>>(&_tables[hashi])) {
            return listFind(*ptr);
        } else if (auto* ptr = std::any_cast<std::set<K>>(&_tables[hashi])) {
            return setFind(*ptr);
        } else {
            assert(false);
            return false;
        }
    }

private:
    std::vector<std::any> _tables;
};

5:std::any和std:variant的对比

维度 std::variant std::any
类型范围 编译期固定所有可能的类型 运行时可存储任意可拷贝类型
类型检查 编译期检查,visit 强制覆盖所有分支 运行时检查,转换时才验证类型
存储位置 完全栈上,共享内存 小对象栈上,大对象堆分配
访问性能 极高,visit 通常优化为跳转表,接近 switch 较低,需要运行时类型判断、指针转换
类型安全 极高,编译期保证类型合法 中等,运行时检查,比 void * 安全但弱于 variant
适用场景 类型数量固定、已知的多类型场景 类型完全不确定、需要极致灵活性的场景

结论 :能确定类型范围时优先用 std::variant,性能和安全性都更优;只有当类型完全无法在编译期确定时,才使用 std::any

3:std::string_view零拷贝字符串试图

1:字符串的冗余拷贝

C++的std::string是所有权的字符串类,每次传参,截取字串都会产生内存分配和数据拷贝,在高频调用的字符串处理场景下开销非常大。

两个最常见的性能浪费场景:

  1. 函数传参的临时对象 :形参为 const std::string& 时,传入字符串字面量会构造临时 std::string 对象,触发堆分配
  2. 字符串解析的子串拷贝 :调用 substr() 拆分字符串时,每个子串都会生成新的 std::string,大量冗余拷贝

2:设计理念与具体用法

std::string_view非拥有(non-owning)的只读字符串视图,它本身不分配内存,只保存指向原字符串的指针和长度,相当于一个观察窗口,零拷贝地查看字符串内容。

核心设计原则

  • 非所有权:不管理原字符串的生命周期,只负责观察
  • 只读性:只能读取字符,不能修改原字符串内容
  • 零开销 :仅包含 const char*size_t 两个成员,构造拷贝成本极低

构造方式

cpp 复制代码
#include <iostream>
#include <string>
#include <string_view>

int main() {
    // 1. 从C风格字符串构造
    std::string_view sv1("Hello, world!");

    // 2. 从std::string隐式转换构造
    std::string str = "C++17 string_view";
    std::string_view sv2(str);

    // 3. 从部分字符串构造(指针+长度)
    std::string_view sv3(str.c_str() + 6, 6); // 取从第6位开始的6个字符

    // 4. 字面量后缀 sv(C++17)
    using namespace std::literals;
    std::string_view sv4 = "Literal"sv;

    return 0;
}

常用接口

std::string_view 提供了和 std::string 几乎一致的只读接口,学习成本极低:

  • 容量:size()empty()length()
  • 访问:operator[]data()front()back()
  • 查找:find()rfind()find_first_of()
  • 截取:substr(pos, len) ------ 注意返回的还是 string_view,零拷贝
  • 比较:支持和 string、字符串字面量的比较运算

3:底层原理

std::string_view 的底层极其简单,就是两个成员变量,没有任何堆分配:

cpp 复制代码
// 简化版底层结构
class string_view {
private:
    const char* data_; // 指向原字符串的指针
    size_t size_;      // 视图的长度
};
  • 大小固定为两个指针大小(64 位系统 16 字节),完全在栈上
  • 所有操作都是指针和长度的计算,没有内存分配和数据拷贝
  • substr() 只是调整指针和长度,不会生成新的字符串副本,性能极高

4:两大核心使用场景

1:函数参数代替const std::string&

这是 string_view 最常用的场景,作为只读字符串参数,避免临时对象拷贝:

cpp 复制代码
#include <string_view>

// 替代 const std::string&,接受任意字符串类型,零拷贝
void process_string(std::string_view sv) {
    // 只读处理sv,和使用string几乎一样
}

int main() {
    process_string("C-string");     // 无临时对象,零拷贝
    std::string s("std::string");
    process_string(s);              // 隐式转换,零开销
    return 0;
}
2:零拷贝字符串解析

字符串拆分、提取子串等解析场景,全程使用 string_view,完全避免拷贝:

cpp 复制代码
#include <string_view>
#include <vector>

// 提取分隔符前的子串,零拷贝返回
std::string_view extract_str(std::string_view input, char delimiter) {
    size_t pos = input.find(delimiter);
    return input.substr(0, pos);
}

// 按分隔符拆分字符串,返回视图数组,全程零拷贝
std::vector<std::string_view> split(std::string_view str, char delimiter) {
    std::vector<std::string_view> result;
    size_t start = 0;
    size_t end = str.find(delimiter);

    while (end != std::string_view::npos) {
        result.push_back(str.substr(start, end - start));
        start = end + 1;
        end = str.find(delimiter, start);
    }
    result.push_back(str.substr(start));
    return result;
}

整个拆分过程没有任何字符串拷贝,仅操作指针和长度,性能远高于基于 std::string 的实现。

5:性能对比

cpp 复制代码
#include <iostream>
#include <chrono>
#include <string>
#include <string_view>

void process_string(const std::string& s) {}
void process_string_view(std::string_view sv) {}

int main() {
    const int N = 100'0000;
    std::string long_str = "This is a very long string...";

    // 测试std::string参数
    auto start1 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        process_string(long_str);     // 无额外开销
        process_string("literal");    // 创建临时string,有分配
    }
    auto end1 = std::chrono::high_resolution_clock::now();

    // 测试std::string_view参数
    auto start2 = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < N; ++i) {
        process_string_view(long_str); // 隐式转换,零开销
        process_string_view("literal");// 无临时对象
    }
    auto end2 = std::chrono::high_resolution_clock::now();

    auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1);
    auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2);
    std::cout << "std::string: " << duration1.count() << "ms\n";
    std::cout << "std::string_view: " << duration2.count() << "ms\n";
    return 0;
}

典型结果:传入字面量的场景下,string_view 的性能可以比 string 快数倍到数十倍,差异主要来自临时对象的堆分配开销。

6:致命陷阱与注意事项

std::string_view 虽然高效,但有三个极易踩的陷阱:

1:生命周期悬垂

string_view 不拥有字符串,它的生命周期不能超过原字符串的生命周期。返回局部字符串的 string_view 是典型错误:

cpp 复制代码
std::string_view get_view() {
    std::string temp = "Temporary string";
    return temp; // 严重错误!temp函数结束时销毁,返回的视图指向已释放内存
}

void error_example1() {
    std::string_view sv = get_view();
    std::cout << sv << std::endl; // 未定义行为,悬垂引用
}
2:不一定以'\0'结尾

std::string::c_str() 保证返回以空字符结尾的 C 字符串,但 string_view::data() 不保证末尾有 '\0'。如果视图是原字符串的子串,data () 指向的位置后面不一定是空字符。

cpp 复制代码
void error_example2() {
    char buffer[] = {'T', 'e', 's', 't', '.', 't', 'x', 't'}; // 无\0结尾
    std::string_view sv(buffer, 4); // 正确,指定长度

    // 错误:传给期望\0结尾的C接口,会越界读取
    FILE* fout = fopen(sv.data(), "w"); 
}

如果需要传给 C 风格接口,必须手动构造 std::string 副本。

3:原字符串修改导致视图失效

如果原字符串发生了修改(尤其是触发扩容的修改),string_view 会失效:

cpp 复制代码
void error_example3() {
    std::string str = "Hello";
    std::string_view sv = str;

    str[0] = 'h'; // 未扩容,视图仍然有效
    std::cout << sv << std::endl;

    // 触发扩容,str重新分配内存,原内存释放,sv变为悬垂
    str = "New value 111111111111111111111111"; 
    std::cout << sv << std::endl; // 未定义行为
}

4:常见陷阱与最佳实践

1:std::any最佳实践

  • 优先用 variant,any 是最后选项:能在编译期确定类型范围的场景,一律用 variant,性能和安全性都更优
  • 取值优先用指针形式std::any_cast<T>(&any) 不会抛异常,适合类型不确定的分支判断场景
  • 避免频繁类型转换:运行时类型检查有开销,不要在高频循环中反复做 any_cast
  • 不要存储不可拷贝类型:any 要求存储的类型必须可拷贝,不可拷贝类型无法存入

2:std::string_view最佳实践

  • 只读函数参数首选 string_view :所有只读取不修改的字符串参数,优先用 std::string_view,替代 const std::string&const char*
  • 绝对不要用 string_view 存储字符串:它只是视图,不是容器,永远不要用它来持有字符串数据
  • 注意生命周期边界
    • 函数参数传入的 string_view,只在当前函数调用内使用,不要存下来
    • 不要返回局部字符串的 string_view
  • 调用 C 接口前检查结尾 :需要传 C 风格字符串时,先构造 std::string 临时对象再调用

5:总结

  1. std::any :运行时类型安全的通用容器,替代不安全的 void*,自带生命周期管理和类型检查,小对象有 SBO 优化,适合类型完全不确定的灵活场景
  2. std::string_view:零拷贝只读字符串视图,仅包含指针和长度,彻底消除传参、解析场景的冗余字符串拷贝,性能极高,但必须注意生命周期问题

结合上一篇的 optional 和 variant,四个工具覆盖了从 "可空值"、"多类型固定值" 到 "任意类型值"、"字符串视图" 的全场景,是现代 C++ 提升代码健壮性与性能的核心武器。

相关推荐
fpcc1 小时前
跟我学C++中级篇—内存流
开发语言·c++
Cicada1281 小时前
ccvt:一个用 Rust 写的中国地图坐标系互转命令行工具
开发语言·后端·rust
1001101_QIA1 小时前
工控机网络配置
开发语言·数据库·php
程序员雷欧2 小时前
ThreadPoolExecutor 深度解析:从核心参数到源码实现的全面剖析
java·开发语言·jvm
catchadmin2 小时前
对标 npx 的 CPX PHP 的 Composer 包执行器
开发语言·php·composer
一位狮子座的程序员2 小时前
如何用RAG解决AI智能体的知识盲区?
开发语言·c#
吹什么轩3 小时前
c++复习:模拟实现list来更好的理解和应用list
开发语言·c++
小白学大数据3 小时前
Python 爬虫实战:抓取汽车之家二手车成交价格与里程数据
开发语言·爬虫·python·汽车
AmyLin_20013 小时前
PDF 色彩保真工程实践【5】核心实现(下):拼装 Image XObject、页面资源与内容流
c++·pdf·sdk·颜色空间·印刷·icc·cmyk