如何在无基类场景下检测void*指针指向的运行时类型
嘿,这个问题确实有点绕——毕竟void*把所有类型信息都抹得一干二净,而且你还说了没有基类和虚方法,常规的dynamic_cast、typeid那套完全派不上用场。不过结合C14/17的特性,还有你提到的“调用者可能知晓真实类型”这个前提,我们还是有几个靠谱的方案的,甚至可以看看未来C版本的思路:
既然调用者知道真实类型,那最稳妥的方式就是让他们额外传一个“类型标签”,比如枚举或者std::type_index,函数里根据标签来做类型转换和判断。
举个代码例子:
#include <vector> enum class TypeTag { Int, VectorInt }; void f(void* p, TypeTag tag) { switch(tag) { case TypeTag::Int: { auto* int_ptr = static_cast<int*>(p); // 这里可以安全处理int类型,比如打印值: printf("This is an int: %d\n", *int_ptr); break; } case TypeTag::VectorInt: { auto* vec_ptr = static_cast<std::vector<int>*>(p); // 处理vector<int>,比如打印元素个数: printf("This is a vector<int> with %zu elements\n", vec_ptr->size()); break; } // 后续要支持新类型,直接加case就行 } } // 调用示例: int main() { int x = 42; f(&x, TypeTag::Int); std::vector<int> vec = {1,2,3}; f(&vec, TypeTag::VectorInt); return 0; }
这个方案的优点是简单高效、完全符合标准、没有未定义行为;缺点是依赖调用者正确传递类型标签,如果传错了(比如把VectorInt当成Int传),就会出现严重的错误。
std::any替代void*(更安全的类型擦除) 如果可以避免直接使用void*,换成C++17引入的std::any会省心很多——它内部会自动保存类型信息,我们可以通过std::any_cast来判断和转换类型。
代码示例:
#include <any> #include <vector> void f(std::any p) { if (auto* int_ptr = std::any_cast<int>(&p)) { printf("This is an int: %d\n", *int_ptr); } else if (auto* vec_ptr = std::any_cast<std::vector<int>>(&p)) { printf("This is a vector<int> with %zu elements\n", vec_ptr->size()); } } // 调用示例: int main() { f(42); f(std::vector<int>{1,2,3}); return 0; }
如果必须用void*传递,也可以让调用者先把对象装进std::any再转成void*,函数里再转回来,但这样有点多此一举,不如直接用std::any作为参数。
如果你的环境只能用C14,不想等C17的std::any,可以自己手动实现一个极简的类型擦除机制——本质就是用一个带虚函数的基类,为每个目标类型写包装类,把类型信息“存”起来。
代码示例:
#include <vector> // 类型擦除的基类,定义判断类型的接口 class TypeErased { public: virtual ~TypeErased() = default; virtual bool is_int() const = 0; virtual bool is_vector_int() const = 0; virtual void* get_raw_ptr() = 0; }; // 针对int的包装类 class IntWrapper : public TypeErased { int* ptr_; public: IntWrapper(int* p) : ptr_(p) {} bool is_int() const override { return true; } bool is_vector_int() const override { return false; } void* get_raw_ptr() override { return ptr_; } }; // 针对vector<int>的包装类 class VectorIntWrapper : public TypeErased { std::vector<int>* ptr_; public: VectorIntWrapper(std::vector<int>* p) : ptr_(p) {} bool is_int() const override { return false; } bool is_vector_int() const override { return true; } void* get_raw_ptr() override { return ptr_; } }; // 目标函数 void f(void* p) { auto* erased_obj = static_cast<TypeErased*>(p); if (erased_obj->is_int()) { auto* int_ptr = static_cast<int*>(erased_obj->get_raw_ptr()); printf("This is an int: %d\n", *int_ptr); } else if (erased_obj->is_vector_int()) { auto* vec_ptr = static_cast<std::vector<int>*>(erased_obj->get_raw_ptr()); printf("This is a vector<int> with %zu elements\n", vec_ptr->size()); } } // 调用示例: int main() { int x = 42; IntWrapper int_wrap(&x); f(&int_wrap); std::vector<int> vec = {1,2,3}; VectorIntWrapper vec_wrap(&vec); f(&vec_wrap); return 0; }
这个方案兼容C++14,但缺点是需要为每个要支持的类型写包装类,有点繁琐,不过比直接裸用void*安全得多。
std::variant+模式匹配(C20/C++23) 如果能用到C20及以上,std::variant是比void*好得多的选择——它是类型安全的联合体,能存储指定的几种类型。配合C20的std::visit(或者C++23的模式匹配),可以非常优雅地判断类型:
代码示例(C++20版本,用std::visit):
#include <variant> #include <vector> #include <iostream> void f(std::variant<int, std::vector<int>> var) { std::visit([](auto&& arg) { using T = std::decay_t<decltype(arg)>; if constexpr (std::is_same_v<T, int>) { std::cout << "This is an int: " << arg << "\n"; } else if constexpr (std::is_same_v<T, std::vector<int>>) { std::cout << "This is a vector<int> with " << arg.size() << " elements\n"; } }, var); } // 调用示例: int main() { f(42); f(std::vector<int>{1,2,3}); return 0; }
到了C++23,还可以用模式匹配的switch语句,代码会更简洁直观:
// C++23模式匹配版本 void f(std::variant<int, std::vector<int>> var) { switch(var) { case int i: std::cout << "This is an int: " << i << "\n"; break; case std::vector<int> vec: std::cout << "This is a vector<int> with " << vec.size() << " elements\n"; break; } }
如果必须用void*,也可以让调用者把variant转成void*传递,但同样不如直接用variant安全。
⚠️ 重要提醒:绝对不要尝试通过内存大小、内存布局或者某个特定内存值来判断类型,比如检查sizeof(*p)或者内存里的某个魔法值——这是标准明确禁止的未定义行为,不同编译器、不同平台的内存布局差异极大,完全不可靠!
内容的提问来源于stack exchange,提问作者km9c3uwc

