这个基于C++11的类型擦除实现是安全的吗,还是会导致未定义行为?

编程语言 2026-07-10

我想出了一个类似 std::any 的东西的C++11实现。根据我的测试,它工作正常,但我担心自己在某处可能触发了未定义行为,尚未察觉。

我特别担心 Any::get() 的实现,以及我这个 Any 类与多态类型之间的交互(子对象的地址不一定等同于父对象的地址)。

我的实现安全吗?

我的实现:

#include <typeinfo>
#include <memory>

class Any {
private:
    template <typename T>
    class TypeSelector {};

    const std::type_info &m_type;
    std::unique_ptr<void, void(*)(const void *)> m_value;

    template <typename T, typename... Args>
    Any(TypeSelector<T>, Args &&... args) :
        m_type(
            typeid(T)
        ),
        m_value(
            std::unique_ptr<void, void(*)(const void *)> {
                new T (
                    std::forward<Args>(args)...
                ),
                [](const void *ptr) {
                    const T *self = static_cast<const T *>(ptr);
                    delete self;
                }
            }
        )
    {}

public:
    Any(Any &&) = default;

    template <typename T, typename... Args>
    static Any make(Args &&... args) {
        return Any(TypeSelector<T> {}, std::forward<Args>(args)...);
    }

    template <typename T>
    T *get() {
        if (m_type == typeid(T)) {
            return static_cast<T *>(m_value.get());
        } else {
            return nullptr;
        }
    }
};

示例用法:

#include <string>
#include <iostream>

template <typename T>
void print(const T *value) {
    if (value) {
        std::cout << *value << '\n';
    } else {
        std::cout << "nullptr" << '\n';
    }
}

int main() {
    Any str = Any::make<std::string>("Hello there");
    Any num = Any::make<int>(54);

    print(str.get<std::string>()); // Hello there
    print(str.get<int>()); // nullptr

    print(num.get<std::string>()); // nullptr
    print(num.get<int>()); // 54

    return 0;
}

Demo

解决方案

我会把指向typeinfo的指针替代引用。这会让移动操作更稳妥。(不过请把空指针也视为空指针处理。)

输入和输出的类型几乎要完全匹配。

不会产生未定义行为。

在动态链接环境中,某些在创建它们的DLL卸载后才失效的any可能会崩溃。我用ADL技巧来确保any在类型库中被创建来解决这个问题。

当你用模板来创建一个函数指针时,该指针是在模板被实例化的那个动态库中创建的。如果在调用该指针之前该动态库就被卸载,程序就会崩溃,因为相应的代码不再存在。

template<class T>
struct tag_t {};

struct bob {
  template<class T, class...Args>
  bob( tag_t<T>, Args&&...args ):
    m_pData( new T{std::forward<Args>(args)...} ),
    m_pDestroy( +[](void* ptr){ delete static_cast<T*>(ptr); } )
  {}

  ~bob() { if (m_pDestroy) m_pDestroy(m_pData); }

  bob(bob&& o) {
    using std::swap;
    swap(m_pData, o.m_pData);
    swap(m_pDestroy, o.m_pDestroy);
  }

  bob& operator=(bob&& o) {
    using std::swap;
    swap(m_pData, o.m_pData);
    swap(m_pDestroy, o.m_pDestroy);
    return *this;
  }
private:
  void* m_pData = nullptr;
  void(*m_pDestroy)(void*) = nullptr;
};

这是一个非常简单的any,名为 bob,它只支持销毁所存储的内容并将数据移动到另一个 bob 中。(你甚至无法访问对象本身。)

问题在于m_pDestroy究竟存放在哪里。

设想我们有一个 chicken.dll、一个 coop.dll 和一个 delivery.dll。这个coop存储着一堆 bob 对象,但并不知道 delivery.dll

我们把鸡放在 bobs中,存在一个 coop.dll 的全局结构里。这些 bobs是在 farm.dll 创建的;因此 m_pDestroy 代码位于 delivery.dll

如果在 coop.dll 被卸载之前,任一 delivery.dllchicken.dll 被卸载,当我们销毁那些 bob 对象时,m_pDestroy 将访问已不再存在于内存中的代码。就会崩溃。

你可以使用一些技巧来确保 m_pDestroy 存在于某个特定的DLL中,例如:

using Destroyer = void(*)(void*);

namespace Birds {
  Destroyer get_dll_safe_destroyer( tag_t<Chicken> );
}

get_dll_safe_destroyer 放在与 Chicken 相同的命名空间里。

然后在bob我们执行:

struct bob {
  template<class T, class...Args>
  bob( tag_t<T>, Args&&...args ):
    m_pData( new T{std::forward<Args>(args)...} ),
    m_pDestroy( get_dll_safe_destroyer(tag_t<T>{}) )
  {}

  ~bob() { if (m_pDestroy) m_pDestroy(m_pData); }

  bob(bob&& o) {
    using std::swap;
    swap(m_pData, o.m_pData);
    swap(m_pDestroy, o.m_pDestroy);
  }

  bob& operator=(bob&& o) {
    using std::swap;
    swap(m_pData, o.m_pData);
    swap(m_pDestroy, o.m_pDestroy);
    return *this;
  }
private:
  void* m_pData = nullptr;
  void(*m_pDestroy)(void*) = nullptr;
};

再加上一个

namespace bob_helpers{
  template<class T>
  Destroyer implement_dll_safe_destroyer( tag_t<T> ) {
    return +[](void* ptr){ delete static_cast<T*>(ptr); };
  }
}

并在 chicken.dll 内部导出

Destroyer Birds::get_dll_safe_destroyer( tag_t<Chicken> tag ) {
  return implement_dll_safe_destroyer(tag);
}

现在每个带有 Chickenbob 都会从 chicken.dll 获得 m_pDestroy 函数,而不是来自于创建它的那个包含 bobChicken 所在的dll。

这并不总是成为问题,因为它要求你的类型擦除对象在DLL卸载期间仍然存在,并且那些在 chicken.dllChicken 放入 bob 之前就被卸载的库也会遇到问题。因此,并不总是会触发问题。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章