禁用RTTI时能否向上转型类型擦除后的对象?
我实现了一个自定义的std::expected,采用类型擦除的方式存储错误,用法类似异常:
Expected<int> V = 123; V.SetError(std::string("Error occurred"));
获取值时可直接输出存储的内容(只要类型支持std::string重载),这种无异常的错误处理方式很实用。
但我希望能像try/catch那样按父类处理错误,示例如下:
struct MathError{}; struct ZeroDivisionError : MathError{}; Expected V = 321; V.SetError(ZeroDivisionError{}); if (auto Error = V.Catch<MathError>()) { // 若错误是MathError的子类则进入此处 }
但项目禁用了RTTI,无法用dynamic_cast把存储为void*的错误向上转型为MathError*。我尝试过用静态模板函数为每个错误类型生成唯一ID,但只能匹配精确类型。类似std::bases的特性或许能枚举基类ID,但它不是标准特性。
请问有没有可行的解决办法?
方案1:手动注册继承关系
为每个错误类型手动维护其所有基类的类型ID集合,在存储错误时一并保存这些ID。调用Catch<T>时,只需检查存储的ID列表中是否包含T的ID即可。
核心实现示例:
// 生成唯一类型ID的工具(编译期生成,无RTTI依赖) template <typename T> struct TypeId { static constexpr size_t value = reinterpret_cast<size_t>(&TypeId<T>::value); }; // 错误存储的内部结构 struct ErrorStorage { void* ptr; void (*deleter)(void*); std::vector<size_t> related_type_ids; // 存储自身+所有基类的ID }; template <typename E> void Expected::SetError(E&& e) { auto* error_ptr = new E(std::forward<E>(e)); // 手动列出当前类型的所有基类ID std::vector<size_t> ids = { TypeId<E>::value, TypeId<MathError>::value // 若E是ZeroDivisionError,需手动添加父类ID }; _error = ErrorStorage{ error_ptr, [](void* p){ delete static_cast<E*>(p); }, std::move(ids) }; } template <typename T> T* Expected::Catch() { if (!_error) return nullptr; // 检查目标类型ID是否在列表中 if (std::find(_error->related_type_ids.begin(), _error->related_type_ids.end(), TypeId<T>::value) != _error->related_type_ids.end()) { // 继承关系已确认,可安全用static_cast转换 return static_cast<T*>(_error->ptr); } return nullptr; }
这种方式简单直接,但缺点是需要手动维护每个错误类型的基类列表,代码量较大且容易遗漏。
方案2:编译期自动收集继承链(C++17+)
利用模板元编程和编译期特性(std::is_base_of无需RTTI),自动推导错误类型的所有基类,并收集它们的ID。如果编译器不支持非标准的std::bases,可以让错误类型手动声明基类列表,再通过模板递归收集。
示例实现:
// 让错误类型手动声明直接基类 struct MathError { using direct_bases = std::tuple<>; }; struct ZeroDivisionError : MathError { using direct_bases = std::tuple<MathError>; }; // 编译期递归收集所有基类ID template <typename T> struct CollectAllTypeIds { using direct_bases = typename T::direct_bases; // 递归收集每个直接基类的ID template <typename Base> using BaseIds = typename CollectAllTypeIds<Base>::type; // 合并自身ID和所有基类ID using type = decltype(std::tuple_cat( std::tuple<size_t>{TypeId<T>::value}, BaseIds<std::tuple_element_t<0, direct_bases>>{}, BaseIds<std::tuple_element_t<1, direct_bases>>{}... // 支持多继承 )); }; // 特化无基类的情况 template <> struct CollectAllTypeIds<void> { using type = std::tuple<>; }; // 将tuple转换为vector的辅助函数 template <typename... Args> std::vector<size_t> tuple_to_vector(const std::tuple<Args...>& t) { return std::apply([](auto... ids){ return std::vector<size_t>{ids...}; }, t); } // 在SetError中使用 template <typename E> void Expected::SetError(E&& e) { auto* error_ptr = new E(std::forward<E>(e)); constexpr auto id_tuple = typename CollectAllTypeIds<E>::type{}; std::vector<size_t> ids = tuple_to_vector(id_tuple); _error = ErrorStorage{ error_ptr, [](void* p){ delete static_cast<E*>(p); }, std::move(ids) }; }
这种方式把基类维护的工作量降到最低,且所有逻辑在编译期完成,无运行时额外开销。
方案3:转换函数表
在存储错误时,为该类型生成一个转换函数表,包含可以转换到所有基类的函数指针。调用Catch<T>时,直接查找对应的转换函数并执行,无需遍历ID列表。
核心实现:
using ConversionFunc = void*(*)(void*); using ConversionTable = std::unordered_map<size_t, ConversionFunc>; struct ErrorStorage { void* ptr; void (*deleter)(void*); ConversionTable converters; }; // 生成转换函数的模板 template <typename Derived, typename Base> void* convert_to_base(void* ptr) { return static_cast<Base*>(static_cast<Derived*>(ptr)); } // 编译期生成转换表 template <typename T, typename... Bases> ConversionTable build_conversion_table() { ConversionTable table; // 添加自身转换 table[TypeId<T>::value] = [](void* p){ return p; }; // 添加直接基类的转换 (table[TypeId<Bases>::value] = &convert_to_base<T, Bases>, ...); // 递归添加基类的基类转换 (merge_tables(table, build_conversion_table<Bases>()), ...); return table; } // 合并两个转换表的辅助函数 void merge_tables(ConversionTable& dest, const ConversionTable& src) { for (const auto& [id, func] : src) { if (!dest.count(id)) { dest[id] = func; } } } // SetError实现 template <typename E> void Expected::SetError(E&& e) { auto* error_ptr = new E(std::forward<E>(e)); auto converters = build_conversion_table<E, typename E::direct_bases>(); _error = ErrorStorage{ error_ptr, [](void* p){ delete static_cast<E*>(p); }, std::move(converters) }; } // Catch实现 template <typename T> T* Expected::Catch() { if (!_error) return nullptr; auto it = _error->converters.find(TypeId<T>::value); if (it != _error->converters.end()) { return static_cast<T*>(it->second(_error->ptr)); } return nullptr; }
这种方式更接近dynamic_cast的底层逻辑,查找效率更高,适合错误类型较多的场景。
内容的提问来源于stack exchange,提问作者Artem Selivanov

