C++ 17 详细特性解析(5)

🎁std::any

std::any 是一个可以存储 任意类型**(必须是可拷贝构造的)** 单个值的容器。当你从 any 中取值时,你必须知道它的原始类型,并通过 std::any_cast 进行安全的转换。如果类型不匹配,它会抛出异常或返回空指针。

variant 类比 union,这里的 any 就类比 void*!

与原始的 void* 不同,any 会记录类型信息,并在 any_cast 时进行检查。其次它管理着自己内部存储的对象生命周期(构造、拷贝、析构),为了避免为小对象频繁分配内存,许多实现使用了小缓冲区优化(SBO,Small Buffer Optimization),MSVC 的std::string就使用了这个优化(小的存在自己的 buffer 当中,大的就去堆上申请,进行管理,好说歹说,栈上开辟空间的效率比堆上开空间的效率要高)。

这里说一下为什么存储的直接任意类型必须是可拷贝构造的:

从底层实现来看,std::any 本质是通过类型擦除 机制存储任意类型的值 ------ 它会在内部为存储的对象分配内存(小对象用栈上的 SBO 缓冲区,大对象用堆),并保存该对象的 "拷贝构造函数 / 析构函数 / 类型信息" 等函数指针;当 std::any 发生拷贝(如赋值、传参、返回)时,必须调用存储类型的拷贝构造函数,将原对象的内容复制到新的内存空间中(否则无法完成值的独立存储),同时 std::any 的 any_cast 等操作也依赖拷贝构造来生成独立的对象实例;如果类型不可拷贝(如删除了拷贝构造函数),std::any 无法完成底层的内存复制和对象生命周期管理,因此标准强制要求存储类型必须是可拷贝构造的。

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

struct MyData {
    std::string content;
    
    // 必须有拷贝构造函数
    MyData(const MyData& other) : content(other.content) {
        std::cout << "MyData拷贝构造被调用\n";
    }
    
    MyData(const char* str) : content(str) {}
};

int main() {
    // 1. 存入数据(发生一次拷贝)
    MyData data1 = "Hello";
    std::any a = data1;  // 调用MyData拷贝构造,复制到any内部
    
    // 2. any拷贝(再发生一次拷贝)  
    std::any b = a;  // any内部再次调用MyData拷贝构造
    
    // 3. any_cast取出(第三次拷贝)
    MyData data2 = std::any_cast<MyData>(b);  // 再次调用拷贝构造
    
    return 0;
}

这里说的直接,其实就是:

std::any 本身强制要求存储的 "值类型" 必须可拷贝构造 ,但我们可以通过指针、智能指针、std::reference_wrapper 等方式 "绕开" 这个限制 ------ 本质是把 "不可拷贝的类型" 包装成 "可拷贝的包装类型",而非直接存储不可拷贝类型本身。:

std::any 存储的直接对象必须是可拷贝构造的(这是标准强制要求,因为其底层类型擦除依赖拷贝构造完成对象的独立存储),但我们可以通过三类 "包装手段" 间接存储不可拷贝类型:

  • ① 存储指针(如 T*):指针本身是可拷贝的,std::any 拷贝的是指针值而非指向的对象,需手动管理对象生命周期;
  • ② 存储智能指针(如 std::unique_ptr<T>/std::shared_ptr<T>):智能指针本身可拷贝 / 移动,std::any 拷贝的是智能指针对象(unique_ptr 仅可移动),由智能指针自动管理对象生命周期;
  • ③ 存储 std::reference_wrapper<T>:它是引用的可拷贝包装器,std::any 拷贝的是包装器而非引用的对象,需保证被引用对象生命周期长于 std::any。这三种方式并非让 std::any 存储不可拷贝类型本身,而是存储 "可拷贝的包装类型",间接实现对不可拷贝对象的 "存储"。

std::any&std::any 对象的引用类型 ,而非 std::any 内部存储的值的引用:它本身不存储任何数据,只是指向已存在的 std::any 对象

当然了,这只是说我在无聊的玩语法

为什么说 any 是类型安全的,体现在哪里:

std::any 被称为类型安全的核心在于:它并非像 void* 那样完全丢弃类型信息,而是在底层通过类型擦除机制存储了值的实际类型信息(std::type_info ,且所有取值操作(std::any_cast)都会在运行时严格校验目标类型与存储类型是否匹配 ------ 若类型不匹配,std::any_cast<T> 会抛出 std::bad_any_cast 异常,std::any_cast<T*> 会返回 nullptr,从根本上避免了 void* 强制类型转换时可能出现的 "类型错误却无提示" 的未定义行为;

同时 std::any 会自动管理存储对象的生命周期,赋值新类型时会先析构旧值、再构造新值,不会出现内存泄漏或野指针问题,这些 "运行时类型校验 + 自动生命周期管理" 的特性,让 std::any 相比无类型校验的裸指针,实现了真正的类型安全。

刚刚说的类型擦除机制又是什么:

理解类型擦除机制,核心是抓住 "抽象基类统一接口 + 模板子类封装具体类型 + 基类指针隐藏细节 " 这三个关键点,下面用极简伪代码还原 std::any 底层的类型擦除核心逻辑:

cpp 复制代码
// -------------------------- 第一步:定义统一的抽象接口(擦除具体类型) --------------------------
// 抽象基类:只声明接口,不关心具体类型,这是"擦除"的核心
struct AnyStorageBase {
    // 析构接口:销毁存储的对象
    virtual ~AnyStorageBase() = default;
    // 拷贝接口:复制存储的对象(返回基类指针,隐藏具体类型)
    virtual AnyStorageBase* clone() const = 0;
    // 类型判断接口:返回存储对象的类型信息
    virtual const std::type_info& get_type() const = 0;
};

// -------------------------- 第二步:模板子类封装具体类型 --------------------------
// 模板子类:为任意类型 T 实现基类接口,包裹具体类型的操作
template<class T>
struct AnyStorage : AnyStorageBase {
    T value; // 真正存储具体类型的值

    // 构造:接收具体类型的值,完成初始化
    AnyStorage(const T& val) : value(val) {}

    // 实现拷贝接口:调用 T 的拷贝构造,返回基类指针(类型擦除)
    AnyStorageBase* clone() const override {
        return new AnyStorage<T>(this->value); // 复制 T 类型的值
    }

    // 实现类型判断接口:返回 T 的类型信息
    const std::type_info& get_type() const override {
        return typeid(T);
    }
};

// -------------------------- 第三步:封装成 std::any 对外接口 --------------------------
class Any {
private:
    AnyStorageBase* storage = nullptr; // 基类指针:只认接口,不认具体类型(擦除核心)

public:
    // 赋值任意类型 T:创建对应的子类对象,用基类指针指向它
    template<class T>
    void set_value(const T& val) {
        delete storage; // 先销毁旧值
        storage = new AnyStorage<T>(val); // 新值封装成子类,基类指针指向它
    }

    // 取值:校验类型,类型匹配则返回值,否则报错(类型安全的关键)
    template<class T>
    T get_value() const {
        // 校验类型:对比存储的类型和目标类型
        if (storage->get_type() != typeid(T)) {
            throw "类型不匹配!"; // 对应 std::bad_any_cast 异常
        }
        // 向下转型:从基类指针转回具体子类指针(恢复类型)
        AnyStorage<T>* concrete_storage = static_cast<AnyStorage<T>*>(storage);
        return concrete_storage->value; // 返回 T 类型的值
    }

    // 拷贝 Any 对象:调用基类的 clone 接口,无需关心具体类型
    Any(const Any& other) {
        if (other.storage) {
            this->storage = other.storage->clone(); // 只调用基类接口,擦除具体类型
        }
    }

    ~Any() { delete storage; } // 析构:调用基类析构,自动销毁子类对象
};

// -------------------------- 测试:验证类型擦除效果 --------------------------
int main() {
    Any a;
    // 存储 int 类型:底层创建 AnyStorage<int>,storage 指向它(基类指针)
    a.set_value(10);
    // 取值:校验类型为 int,返回 10
    std::cout << a.get_value<int>() << std::endl;

    // 存储 string 类型:销毁旧的 int 对象,创建 AnyStorage<string>
    a.set_value(std::string("hello"));
    // 取值:校验类型为 string,返回 "hello"
    std::cout << a.get_value<std::string>() << std::endl;

    // 错误示例:类型不匹配,抛出异常
    // a.get_value<int>(); // 报错:类型不匹配!

    return 0;
}

🔗 参考:https://en.cppreference.com/w/cpp/header/any.html


🛠️ std::any 核心接口

函数 作用
构造函数 std::any any_value = 42; std::any any_value = std::string("Hello");
emplace<T>(args...) 原地构造一个类型为 T 的对象,参数 args 传递给 T 的构造函数。
reset() 销毁内部包含的对象,使 any 变为空。
has_value() 返回一个 bool,判断 any 对象当前是否包含一个值。
type() 返回一个 std::type_info const&,表示当前包含值的类型。如果 any 为空,则返回 typeid(void)
std::any_cast<T> 最重要的函数,用于从 any 对象中提取值。如果转换失败,会抛出 std::bad_any_cast 异常。

💻 构造和赋值

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

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

    std::any a4;
    a4 = std::pair<std::string, std::string>("xxxx", "yyyy");

    // 使用 emplace 原地构造
    a3.emplace<std::string>("World"); // 现在 a3 包含 "World",之前的 "Hello" 被销毁

    std::cout << sizeof(a1) << '\n';
    std::cout << sizeof(a4) << '\n';

    // 检查是否有值
    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;
}

这段代码中体现了 std::any 两种核心构造方式:

  • 一是直接赋值构造 (隐式构造),比如 std::any a1 = 42;a2 = 3.14;a3 = std::string("Hello");,这种方式会调用 std::any 的模板赋值运算符,底层通过类型擦除机制,调用存储类型(int/double/std::string)的拷贝构造函数,将值复制到 std::any 管理的内存中(小对象用栈上 SBO 缓冲区,大对象用堆);
  • 二是默认构造 + 后赋值 ,比如 std::any a4; 先创建空的 std::any 对象(无存储值,has_value() 返回 false),再通过 a4 = std::pair<...> 完成赋值,此时会先初始化空对象的内部存储,再拷贝构造 std::pair 对象;
  • 三是emplace 原地构造 ,代码中 a3.emplacestd::string("World"); 是更高效的方式,它会先销毁 a3 中原有的 "Hello" 字符串(调用 std::string 析构函数),再在 std::any 内部的内存空间直接构造 "World" 字符串,避免了 "先构造临时字符串再拷贝" 的额外开销,是原地创建值的最优方式。

std::any 的赋值核心遵循 "先销毁旧值,再拷贝 / 构造新值" 的规则:比如 a3 最初存储 "Hello",执行 emplace<std::string>("World") 时,底层会先调用原 std::string 的析构函数释放内存,再调用 std::string("World") 的构造函数在原有内存空间创建新值;

a4 = std::pair<...> 这种普通赋值,因为 a4 初始为空,所以直接调用 std::pair 的拷贝构造函数,将 std::pair("xxxx", "yyyy") 复制到 a4 的内存中;

需要注意的是,无论哪种赋值,std::any 都会记录新值的类型信息(比如 int/std::pair),并更新内部的类型擦除相关函数指针(拷贝、析构等),保证后续 type()、any_cast 能正确识别类型。

代码中 sizeof(a1) 和 sizeof(a4) 输出相同(通常为指针大小 + 类型信息存储大小,比如 8/16 字节),这体现了 std::any 的内存特性:无论存储的是 int(4 字节)还是 std::pair(大对象),std::any 对象本身的大小固定 ------ 小对象(如 int/double/ 短字符串)存储在 std::any 内部的 SBO 缓冲区(栈),大对象(如 std::pair、长字符串)存储在堆上,std::any 仅持有指向堆内存的指针,因此对象本身大小不变;同时,构造 / 赋值后可通过 has_value() 检查是否有有效存储值(如 a3.has_value() 返回 true),通过 type() 获取存储值的类型信息(如 a1.type() 返回 int 的 type_info),这些都是构造 / 赋值后类型擦除机制保留的关键信息,保证了类型安全。


🔍 any_cast 三种取值方式

🔗 参考:https://en.cppreference.com/w/cpp/utility/any/any_cast.html

cpp 复制代码
#include <iostream>
#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)) {
            std::cout << "字符串配置:" << std::any_cast<std::string&>(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';
    }

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

    std::string str_ref2 = std::any_cast<std::string&>(a3);
    str_ref2[0]++;
    std::cout << std::any_cast<std::string&>(a3) << '\n';

    std::string&& str_ref3 = std::any_cast<std::string&&>(std::move(a3));
    str_ref3[0]++;
    std::cout << std::any_cast<std::string&>(a3) << '\n';

    std::string str_ref4 = std::any_cast<std::string&&>(std::move(a3));
    str_ref4[0]++;
    std::cout << std::any_cast<std::string&>(a3) << '\n';

    // 方式三:转换为指针(如果类型不匹配,返回 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();
}

C++ 标准中 std::any_cast 主要有 3 个重载版本,对应三种取值方式:

cpp 复制代码
// 方式1:转换为值类型(值拷贝)
template <class T>
T any_cast(const any& operand);

template <class T>
T any_cast(any& operand);

template <class T>
T any_cast(any&& operand); // 这个可不是万能引用,没有引用折叠,又没有自动推导 auto 就是一个右值形参

// 方式2:转换为引用类型(本质是上述模板的自然推导,核心重载如下)
template <class T>
T& any_cast(any& operand);

template <class T>
const T& any_cast(const any& operand);

template <class T>
T&& any_cast(any&& operand);

// 方式3:转换为指针类型
template <class T>
T* any_cast(any* operand) noexcept;

template <class T>
const T* any_cast(const any* operand) noexcept;

std::any_cast 取值方式 1:转换为值类型(值拷贝)

这种方式是直接从 std::any 中拷贝出存储值的副本,代码中 int int_value = std::any_cast<int>(a1); 就是典型示例 ------std::any_cast<T>(any_obj) 会先校验 any_obj 存储的类型是否为 T,若匹配则调用 T 的拷贝构造函数,生成一个独立的 T 类型值;

若类型不匹配(如 std::any_cast<double>(a1)),会直接抛出 std::bad_any_cast 异常,这是类型安全的核心体现。该方式的特点是取值后得到的是 "副本 ",修改副本不会影响 std::any 内部存储的原值(如 str_ref1 修改后,a3 中的字符串仍为 "Hello"),但对于大类型(如长字符串、大容器)会有拷贝开销。

std::any_cast 取值方式 2:转换为引用类型(直接操作原值)

这种方式通过 std::any_cast<T&>(any_obj)std::any_cast<T&&>(std::move(any_obj)) 获取 std::any 内部存储值的引用 ,而非副本:代码中 std::string& str_ref2 = std::any_cast<std::string&>(a3) 拿到的是 a3 中字符串的左值引用,修改 str_ref2 会直接改变 a3 内部的原值("Hello" 变为 "Jello");

std::string&& str_ref3 = std::any_cast<std::string&&>(std::move(a3)) 是获取右值引用,可转移 std::any 内部值的所有权,修改右值引用也会直接改变原值,但需注意右值引用使用后,原 std::any 对象的状态会变为 "有效但未定义"(如 str_ref4 拷贝后,a3 内部字符串已被转移,再访问可能出现空值 / 乱码)。该方式的核心是 "无拷贝、直接操作原值",适合对性能要求高的场景,但需谨慎避免悬垂引用。

std::any_cast 取值方式 3:转换为指针类型(安全校验 + 无异常)

这种方式是通过指针间接访问 std::any 内部的值,代码中 auto ptr = std::any_cast<int>(&a1) 是关键示例 ------std::any_cast<T>(&any_obj) 会校验 any_obj 存储的类型是否为 T若匹配则返回指向内部值的 T* 指针;若类型不匹配或 any_obj 为空,不会抛出异常,而是直接返回 nullptr 该方式的优势是 "非侵入式" 且无异常风险,适合需要优雅处理类型不匹配的场景(如代码中通过 if (ptr) 判断是否为 int 类型),通过指针解引用(*ptr)可直接操作 std::any 内部的原值,兼顾了安全性和灵活性。


🧩 综合运用样例

实现一个简单哈希桶,桶长度大于 8 存到红黑树,桶长度小于等于 8 存到链表,我们可以直接将上面的 variant 版本哈希桶出来尝试改成 any 版本。

cpp 复制代码
#include <any>
#include <cassert>
#include <list>
#include <set>
#include <vector>

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

    void insert(const K& key) {
        size_t hash = key % _tables.size();

        // 链表插入
        auto listInsert = [&key](std::list<K>& lt) -> bool {
            // 小于,则插入到链表
            if (lt.size() < Len) {
                lt.push_back(key);
                return true;
            }
            else {
                // 大于,则转换到红黑树
                std::set<K> s(lt.begin(), lt.end());
                s.insert(key);
                return false;
            }
        };

        // 红黑树插入
        auto setInsert = [&key](std::set<K>& s) {
            s.insert(key);
        };

        if (auto ptr = std::any_cast<std::list<K>>(&_tables[hash])) {
            if (!listInsert(*ptr)) {
                std::set<K> s(ptr->begin(), ptr->end());
                s.insert(key);
                _tables[hash] = std::move(s);
            }
        }
        else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hash])) {
            setInsert(*ptr);
        }
        else {
            assert(false);
        }
    }

    bool find(const K& key) {
        size_t hash = key % _tables.size();
        if (!_tables[hash].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[hash])) {
            return listFind(*ptr);
        }
        else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hash])) {
            return setFind(*ptr);
        }
        else {
            assert(false);
            return false;
        }
    }

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

int main() {
    HashTable<int, 8> ht(10);
    for (int i = 0; i < 10; ++i) {
        ht.insert(i * 10);
    }

    std::cout << ht.find(15) << std::endl;
    std::cout << ht.find(30) << std::endl;

    return 0;
}

这个哈希表案例是 std::any 典型的 "动态类型存储" 应用场景:std::any 被用来定义哈希桶的存储类型,让每个桶既能存储 std::list<K>(链表)也能存储 std::set<K>(红黑树)------ 初始化时桶默认存储链表,插入元素时通过 std::any_cast 指针方式(安全且不抛异常)判断当前桶的类型,若链表元素数超过阈值则将链表转为红黑树并通过 std::any 的赋值(std::move 转移所有权)完成类型替换,查找元素时同样通过 std::any_cast 识别桶的实际类型(链表 / 红黑树),调用对应的查找逻辑;std::any 在这里的核心价值是用类型安全的方式实现 "单一存储位置动态切换不同容器类型" ,无需手动维护类型标记,且通过 any_cast 指针方式避免了异常风险,既满足了 "元素少用链表省空间、元素多用红黑树提效率" 的性能平衡需求,又相比传统 void* 或联合体保证了类型安全和自动内存管理。


🧐 std::any vs std::variant(面试必备)

从功能角度,他们都是用于存储多种不同类型的类型安全的单值容器,使用方法上也有诸多相似。

使用方式std::any 存储任意类型,使用时必须通过 std::any_cast 转换访问;std::variant 存储预定义的有限类型,通过 std::getstd::visit 访问。

安全性std::variant 在编译期就知道所有可能的类型,访问更安全(跳转表的原因,编译的时候就生成了);std::any 类型信息只有运行时才知道,转换失败会抛出异常或返回空指针。

性能 :std::variant 是栈分配,性能更高;std::any 对于大对象会在堆上分配内存,且每次访问都需要类型检查,有一定开销。

适用场景

  • 当你明确知道所有可能的类型时,优先使用 std::variant
  • 当你需要存储完全未知的任意类型时,才使用 std::any
相关推荐
lly2024064 小时前
《堆的 shift down》
开发语言
cpp_25014 小时前
P10570 [JRKSJ R8] 网球
数据结构·c++·算法·题解
cpp_25015 小时前
P8377 [PFOI Round1] 暴龙的火锅
数据结构·c++·算法·题解·洛谷
黎雁·泠崖5 小时前
【魔法森林冒险】2/14 抽象层设计:Figure/Person类(所有角色的基石)
java·开发语言
uesowys5 小时前
Apache Spark算法开发指导-Factorization machines classifier
人工智能·算法
程序员老舅5 小时前
C++高并发精髓:无锁队列深度解析
linux·c++·内存管理·c/c++·原子操作·无锁队列
划破黑暗的第一缕曙光5 小时前
[C++]:2.类和对象(上)
c++·类和对象
季明洵5 小时前
C语言实现单链表
c语言·开发语言·数据结构·算法·链表
shandianchengzi5 小时前
【小白向】错位排列|图文解释公考常见题目错位排列的递推式Dn=(n-1)(Dn-2+Dn-1)推导方式
笔记·算法·公考·递推·排列·考公