You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

禁用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 11:25:02