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

6
在一个复杂的代码库中,我有一个指向非虚基类指针的数组(基类没有虚方法)。
考虑下面的代码:
#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;
}

输出结果为(使用Mingw-w64 GCC 4.9.2在Win64上编译):
Data = 0
Data = 1
Data = 2
Data = 0
Data = 1
Data = 4771632

失败的原因是每个TVirtualDerived实例都有一个指向虚拟表的指针,而TBase没有。因此,在没有先前类型信息的情况下(从void*到TBase*),向上转型为TBase不安全。
问题在于我一开始无法避免强制转换为void*。在基类中添加一个虚方法(例如析构函数)可以解决问题,但会增加内存成本(我想要避免这种情况)
背景:
我们正在一个非常受限制的环境(内存严重受限)中实现信号/槽系统。由于我们有数百万个可以发送或接收信号的对象,因此这种优化是有效的(当然,只有在它起作用时)
问题:
如何解决这个问题?到目前为止,我已经找到了以下解决方法:
1- 在TBase中添加一个虚方法。虽然可行,但并没有真正解决问题,只是避免了问题。而且效率低下(太多内存)
2- 代价是失去通用性,将数组转换为TBase*而不是void*。(可能是我下一步尝试的方案)
您看有其他解决方案吗?

1
将对象先转换为 TBase*,再转换为 void*,这样能否满足您的需求?(请参见此处 - danielschemmel
1
只是为了明确:您的一些派生类具有虚方法,而其他一些则没有。而且TBase本身非常小,添加vtable指针会导致内存大小显着增加。正确吗? - Dale Wilson
你考虑过使用模板吗? - cup
@DaleWilson 大小并不重要。如果TBase里有1000个int,这仍然不起作用。 - Barry
@gha.st:在多重继承的情况下,这种方法是行不通的。请参见帖子中的更新。 - Seb
一个(只在显式使用取地址运算符的情况下有效)的技巧是在TBase中添加成员函数TBase* operator&() { return this; }(以及其const版本)。 - celtschk
3个回答

4
问题出在你的强制类型转换上。由于你使用了一个通过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 ** 数组。

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

给出的代码是为了说明问题(最小示例)。在我们的代码库中(>800,000行代码),我们有一个无类型委托类(请参见此文章的主要思想:委托)。对TBase的转换适用于大多数情况,但是当使用多重继承时,它不再起作用 - 我们需要原始的最派生类型,否则编译器将无法正确推断方法地址。这就是为什么我们将对象指针转换为void - 以保留原始指针的原因。 - Seb

3

您需要考虑类在内存中的布局。TBase很容易,它只有一个成员,占用四个字节:

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

TStaticDerived 是一样的。但是,TVirtualDerived 则完全不同。它现在具有 8 的对齐方式,并且必须在开头包含一个 vtable,其中包含一个析构函数条目:

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

当你将vd转换为void*,然后再转换为TBase*时,你实际上重新解释了你的vtable的前四个字节(指向~TVirtualDerived()的偏移地址)作为m_iData。解决方法是首先进行static_castTBase*,这将返回一个指向vdTBase的正确起始点的指针,然后再转换为void*

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

谢谢您的回答。我已经更新了问题,解释了为什么我们需要使用这个系统。在多重继承的情况下,static_cast无法工作。 - Seb
@Seb 我已经还原了你的编辑,因为那是一堵没有实质性改变问题精神的文字墙。而且 static_cast 绝对可以处理多重继承 - 问题很可能在于你在 void* 上来回转换是错误的。 - Barry

0

忘记虚拟多态。老派的方法才是王道。

在每个TBase中添加一个字节以指示类型,并在打印方法中使用switch语句来“做正确的事情”。(相比虚拟方法方法,这样可以为每个TBase节省sizeof(pointer)-1个字节。)

如果添加一个字节仍然太昂贵,请考虑使用C/C++位域(还有人记得吗(微笑))将类型字段压缩到其他未填满可用空间的字段中(例如,具有最大值2^24-1的无符号整数)

你的代码会很丑陋,没错,但你的内存限制也很丑陋。能工作的丑陋代码总比失败的漂亮代码好。


谢谢您的回答。我们已经尝试了这种方法,并且对大多数类都有效。但是当我们想从TBase和另一个具有虚拟方法的类(来自另一个库)派生时,就会出现问题。这就是为什么我发布这个问题的原因。我们试图编写高效的代码,但它已经非常丑陋!(但有效!)丑陋的问题在于维护成本很高(需要更多时间)。当我们需要在6个月后扩展/修补代码时,我们会后悔的。 - Seb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接