C++ 不安全转换解决方法

C++ unsafe cast workaround

在一个复杂的代码库中,我有一个非虚基 class 指针数组(基 class 没有虚方法)

考虑这段代码:

#include <iostream>

using namespace std;

class TBase
{
    public:
        TBase(int i = 0) : m_iData(i) {}
        ~TBase(void) {}

        void Print(void) {std::cout << "Data = " << m_iData << std::endl;}

    protected:
        int     m_iData;
};

class TStaticDerived : public TBase
{
    public:
        TStaticDerived(void) : TBase(1) {}
        ~TStaticDerived(void)  {}
};

class TVirtualDerived : public TBase
{
    public:
        TVirtualDerived(void) : TBase(2) {}
        virtual ~TVirtualDerived(void) {} //will force the creation of a VTABLE
};

void PrintType(TBase *pBase)
{
    pBase->Print();
}

void PrintType(void** pArray, size_t iSize)
{
    for(size_t i = 0; i < iSize; i++)
    {
        TBase *pBase = (TBase*) pArray[i];
        pBase->Print();
    }
}


int main()
{
    TBase b(0);
    TStaticDerived sd;
    TVirtualDerived vd;

    PrintType(&b);
    PrintType(&sd);
    PrintType(&vd); //OK

    void* vArray[3];
    vArray[0] = &b;
    vArray[1] = &sd;
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK
    PrintType(vArray, 3);

    return 0;
}

输出为(在 Win64 上使用 Mingw-w64 GCC 4.9.2 编译):

Data = 0
Data = 1
Data = 2
Data = 0
Data = 1
Data = 4771632

失败的原因是每个 TVirtualDerived 实例都有一个指向虚拟 table 的指针,而 TBase 没有。因此,在没有先前类型信息的情况下向上转换为 TBase(从 void* 到 TBase*)是不安全的。

问题是我一开始就无法避免强制转换为 void* 。 在基础 class 上添加一个虚拟方法(例如析构函数)是可行的,但会产生内存成本(我想避免)

上下文:

我们正在一个非常受限的环境(内存严重受限)中实施一个 signal/slot 系统。由于我们有数百万个可以发送或接收信号的对象,因此这种优化是有效的(当然,当它起作用时)

问题:

我该如何解决这个问题?到目前为止,我发现:

1 - 在 TBase 中添加虚方法。有效,但它并没有真正解决问题,它避免了它。而且效率低(内存太多)

2 - 转换为 TBase* 而不是转换为数组中的 void*,代价是失去了通用性。 (可能是我接下来会尝试的)

你看到另一个解决方案了吗?

您必须考虑 class 在内存中的布局方式。 TBase很简单,一个成员就四个字节:

 _ _ _ _
|_|_|_|_|
 ^
 m_iData

TStaticDerived也是一样。但是,TVirtualDerived 就完全不同了。它现在的对齐方式为 8,并且必须首先使用 vtable 启动,其中包含析构函数的条目:

 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|_|
 ^               ^
 vtable          m_iData

因此,当您将 vd 转换为 void* 然后转换为 TBase* 时,您实际上是在重新解释 vtable 的前四个字节(偏移地址为 ~TVirtualDerived() ) 作为 m_iData。解决方案是先做一个static_castTBase*,这将return一个指针指向vdTBase的正确起点然后void*:

vArray[2] = static_cast<TBase*>(&vd); // now, pointer is OK

问题出在你的投射上。当您通过 void 使用 C 类型转换 时,它等同于 reinterpret_cast,这在子类化时可能很差。在第一部分中,编译器可以访问类型,并且您的转换等同于 static_cast.

但是我无法理解为什么你说你首先就无法避免转换为 void*。由于 PrintType 在内部会将 void * 转换为 TBase *,您也可以传递 TBase **。在那种情况下它会正常工作:

void PrintType(TBase** pArray, size_t iSize)
{
    for(size_t i = 0; i < iSize; i++)
    {
        TBase *pBase = pArray[i];
        pBase->Print();
    }
}
...
    TBase* vArray[3];
    vArray[0] = &b;
    vArray[1] = &sd;
    vArray[2] = &vd; //VTABLE not taken into account -> pointer not OK
    PrintType(vArray, 3);

或者,如果您想要使用void **数组,您必须明确地确保您放入其中的只是TBase * 而不是指向子类的指针 :

void* vArray[3];
vArray[0] = &b;
vArray[1] = static_cast<TBase *>(&sd);
vArray[2] = static_cast<TBase *>(&vd);
PrintType(vArray, 3);

这两种方法都正确输出:

Data = 0
Data = 1
Data = 2
Data = 0
Data = 1
Data = 2

忘记虚拟多态性吧。用老式的方法来做。

向每个 TBase 添加一个字节以指示类型,并在打印方法中添加一个 switch 语句 "Do The Right Thing."(与虚拟方法方法相比,这为每个 TBase 节省了 sizeof(pointer) -1 字节。

如果添加一个字节仍然太昂贵,请考虑使用 C/C++ 位字段(任何人都记得那些(咧嘴笑))将类型字段压缩到不填充 space 可用(例如,最大值为 2^24 - 1 的无符号整数)

你的代码确实很丑陋,但你严格的内存限制也很丑陋。有效的丑陋代码胜过失败的漂亮代码。