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_cast
到TBase*
,这将return一个指针指向vd
和中TBase
的正确起点然后到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 的无符号整数)
你的代码确实很丑陋,但你严格的内存限制也很丑陋。有效的丑陋代码胜过失败的漂亮代码。
在一个复杂的代码库中,我有一个非虚基 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_cast
到TBase*
,这将return一个指针指向vd
和中TBase
的正确起点然后到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 的无符号整数)
你的代码确实很丑陋,但你严格的内存限制也很丑陋。有效的丑陋代码胜过失败的漂亮代码。