【问题标题】:C++ unsafe cast workaroundC++ 不安全强制转换解决方法
【发布时间】:2015-05-12 21:25:31
【问题描述】:

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

考虑这段代码:

#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的每个实例都有一个指向虚拟表的指针,而TBase没有。因此,在没有先前类型信息的情况下向上转换到 TBase(从 void* 到 TBase*)是不安全的。

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

上下文:

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

问题:

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

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

2 - 转换为 TBase* 而不是转换为数组中的 void*,但会失去一般性。 (可能我接下来会尝试)

你看到其他解决方案了吗?

【问题讨论】:

  • 会简单地转换到TBase* 之前转换到void* 解决问题让您满意吗? (See here)
  • 要明确一点:你的一些派生类有虚方法。其他人没有。而且 TBase 本身很小,添加一个 vtable 指针会导致内存大小显着增加。对吗?
  • 您考虑过使用模板吗?
  • @DaleWilson 大小无关紧要。如果 TBase 中有 1000 个 ints,这仍然行不通。
  • @gha.st :这在多重继承的情况下不起作用。请参阅帖子中的更新。

标签: c++ virtual


【解决方案1】:

问题出在你的演员身上。当您使用 C 类型转换通过 void 时,它等同于 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

【讨论】:

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

您必须考虑类在内存中的布局方式。 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 我撤回了您的编辑,因为它是一堵不会明显改变问题精神的文字墙。此外,static_cast 绝对适用于多重继承 - 问题很可能是您来回转换到 void* 是不正确的。
【解决方案3】:

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

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

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

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

【讨论】:

  • 感谢您的回答。我们已经尝试过这种方法,它适用于大多数课程。但是当我们想从 TBase 和另一个类(来自另一个库)派生时,我们遇到了一个问题。这就是我发布问题的原因。我们尝试做高效的代码,它已经很丑陋了! (但有效!)。丑陋的问题是维护:它很昂贵(需要更多时间)。而当我们需要在 6 个月后扩展/修补代码时,我们会后悔的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-05-05
  • 1970-01-01
  • 2020-10-12
  • 1970-01-01
  • 2015-12-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多