对可能不完整的类型进行可选的安全检查强制转换

Optionally safety-checked cast on possibly incomplete type

本文关键字:安全检查 转换 类型      更新时间:2023-10-16

根据一个简单的、侵入式引用计数的对象系统,我有一个template<typename T> class Handle,它打算用CountedBase的一个子类实例化。Handle<T>持有一个指向T的指针,并且它的析构函数在该指针上调用DecRef(在CountedBase中定义)。

通常,当尝试使用前向声明来限制头文件依赖时,这会导致问题:

#include "Handle.h"
class Foo; // forward declaration
struct MyStruct {
    Handle<Foo> foo; // This is okay, but...
};
void Bar() {
    MyStruct ms;
}   // ...there's an error here, as the implicit ~MyStruct calls
    // Handle<Foo>::~Handle(), which wants Foo to be a complete
    // type so it can call Foo::DecRef(). To solve this, I have
    // to #include the definition of Foo.

作为解决方案,我重写了Handle<T>::~Handle()如下:

template<typename T>
Handle<T>::~Handle() {
    reinterpret_cast<CountedBase*>(m_ptr)->DecRef();
}

请注意,我在这里使用reinterpret_cast而不是static_cast,因为reinterpret_cast不需要完整的T定义。当然,它也不会为我执行指针调整…但只要我小心布局(T必须将CountedBase作为其最左边的祖先,不能虚拟地继承它,并且在一些不寻常的平台上,一些额外的虚表魔法是必要的),它是安全的。

如果我能在可能的情况下获得额外的static_cast安全层,那么真正的是什么呢?在实践中,T的定义通常在Handle::~Handle实例化时就已经完成了,这使得再次检查T是否确实继承自CountedBase成为了一个完美的时机。如果它不完整,我就无能为力了……但是如果它是完整的,那么一个完整性检查将是很好的。

这就引出了我的问题:是否有任何方法来做一个编译时检查,TCountedBase继承,当T不完整时不会导致(虚假)错误?

通常的免责声明:我知道以这种方式使用不完整类型存在潜在的不安全和/或UB方面。然而,在进行了大量的跨平台测试和分析之后,我已经确定,考虑到我的用例的某些独特方面,这是最实用的方法。我对编译时检查问题感兴趣,而不是一般的代码检查。[/p>

sizeof上使用SFINAE检查类型是否完整:

struct CountedBase {
    void decRef() {}
};
struct Incomplete;
struct Complete : CountedBase {};
template <std::size_t> struct size_tag;
template <class T>
void decRef(T *ptr, size_tag<sizeof(T)>*) {
    std::cout << "staticn";
    static_cast<CountedBase*>(ptr)->decRef();
}
template <class T>
void decRef(T *ptr, ...) {
    std::cout << "reinterpretn";
    reinterpret_cast<CountedBase*>(ptr)->decRef();
}
template <class T>
struct Handle {
    ~Handle() {
        decRef(m_ptr, nullptr);
    }
    T *m_ptr = nullptr;
};
int main() {
    Handle<Incomplete> h1;
    Handle<Complete> h2;
}

输出(注意销毁顺序是颠倒的):

static
reinterpret

Live on Coliru

使用不派生自CountedBase的完整类型进行尝试会得到:

main.cpp:16:5: error: static_cast from 'Oops *' to 'CountedBase *' is not allowed

话虽这么说,我认为更优雅(更显式)的方法是引入一个类模板incomplete<T>,这样Handle<incomplete<Foo>>就会编译为reinterpret_cast,而其他任何东西都试图编译为static_cast