类型安全的通用指针






4.82/5 (18投票s)
any_ptr 是 void* 的一个更安全的替代方案,它可以指向任何类型的对象,并提供对其进行类型安全、const 正确的访问。
引言
有时需要存储一个类型在编译时未知的对象的指针。在 C++ 中实现此目的的常用方法是将其存储在 void 指针中。然后,可以在需要时将 void 指针转换回适当的类型并使用它。库中的回调函数[1]是这种方法的一个著名例子;用户数据指针通常是一个 void 指针。这可行,但有一个明显的缺点:它不类型安全;程序员需要负责以适当的方式解释 void 指针。
尽管程序员在处理 void 指针时非常习惯于小心谨慎,但偶尔也会发生 void 指针意外转换为错误类型对象的情况。发生这种情况时,如果出现崩溃,程序员实际上算是幸运的。
引入 any_ptr
any_ptr
智能指针[2]可以指向任何类型的对象,并为其提供类型安全的访问。在某种意义上,any_ptr
对于指针来说,就像 boost::any
[3] 对于对象一样。any_ptr
可以指向的对象类型没有任何限制。事实上,其使用带来的性能和大小损失非常低,以至于几乎可以在任何需要 void 指针的地方使用它。
要开始使用 any_ptr
,只需将一个头文件 any_ptr.h(请参阅本文附带的源代码)添加到我们的项目中。any_ptr
不依赖任何库,可以独立使用。any_ptr
也不需要启用异常[5]或 RTTI[6]。
类型安全性
any_ptr
的行为几乎与 void 指针相同,除了在进行类型转换时。转换为错误的指针类型将产生一个空指针,程序员可以对其进行检查。
int a = 10;
any_ptr pAny = &a;
int *pA = pAny; // 'pA' will point to 'a'; type-compatible
float *pB = pAny; // 'pB' will point to null; type-incompatible
Const 正确性
any_ptr
也负责 Const 正确性[4];与 Const 正确性不兼容的类型转换将产生一个空指针。
int a = 10;
any_ptr pAny = &a;
const int *p1 = pAny; // 'p1' will point to 'a'; const-compatible
int *p2 = pAny; // 'p2' will point to 'a'; const-compatible
const int a = 10;
any_ptr pAny = &a;
const int *p1 = pAny; // 'p1' will point to 'a'; const-compatible
int *p2 = pAny; // 'p2' will point to null; const-incompatible
示例:将用户数据传递给回调函数
代替使用原始的 void 指针,可以使用 any_ptr
将用户定义的数据传递到回调函数中。这将为回调函数的作者提供对用户定义数据的类型安全访问。考虑以下伪代码
#include <map>
#include <cassert>
// Call back for key presses
typedef void (*KeyPressedCallback)(void *pUserData);
class KeyPressNotifier
{
private:
struct RegistrationInfo
{
KeyPressedCallback _pCallback;
void *_pUserData;
};
typedef std::map<int, RegistrationInfo> InfoMap;
InfoMap _infoMap;
public:
void Register(int key, KeyPressedCallback pCallback, void *pUserData)
{
RegistrationInfo &info = _infoMap[key];
info._pCallback = pCallback;
info._pUserData = pUserData;
}
void UpdateKeyPress(int key)
{
InfoMap::const_iterator itr = _infoMap.find( key );
if( itr != _infoMap.end() )
itr->second._pCallback( itr->second._pUserData );
}
};
void OnAccelerate(void *pUserData)
{
// Expect the speed to be passed in
float *pSpeed = static_cast<float*>(pUserData);
assert( pSpeed );
// Increment speed
++(*pSpeed);
}
void OnDrinkPotion(void *pUserData)
{
// Expect the health to be passed in
int *pHealth = static_cast<int*>(pUserData);
assert( pHealth );
// Increment health
++(*pHealth);
}
enum Keys
{
UpArrowKey,
EnterKey
};
int main(int /*argc*/, char * /*argv*/[])
{
KeyPressNotifier notifier;
float speed = 10.5f;
notifier.Register( UpArrowKey, OnAccelerate, &speed );
float health = 100;
notifier.Register( EnterKey, OnDrinkPotion, &health );
notifier.UpdateKeyPress( UpArrowKey );
notifier.UpdateKeyPress( EnterKey );
assert( speed == 11.5f );
assert( health == 101 );
}
乍一看,上面的代码似乎没问题,但有一个 bug。在 OnDrinkPotion
函数中,玩家的生命值被期望作为整数传递。如果传递了 null,则会触发断言失败。现在,尽管玩家的生命值被传递了,但其类型是 float
。在这种情况下,由于从 void 指针到用户数据的指针无效但不是 null,因此不会触发断言失败。在这个简单的示例中,这个 bug 的后果并不严重。但想象一下,这个简单示例的现实对应场景,其中复杂的对象被用作用户数据而不是原始类型。
在这种情况下,只需将 void 指针替换为 any_ptr
即可提高类型安全性。以下是与上面相同的代码,但使用了 any_ptr
而不是 void 指针
#include "any_ptr.h"
#include <map>
#include <cassert>
// Call back for key presses
typedef void (*KeyPressedCallback)(any_ptr pUserData);
class KeyPressNotifier
{
private:
struct RegistrationInfo
{
KeyPressedCallback _pCallback;
any_ptr _pUserData;
};
typedef std::map<int, RegistrationInfo> InfoMap;
InfoMap _infoMap;
public:
void Register(int key, KeyPressedCallback pCallback, any_ptr pUserData)
{
RegistrationInfo &info = _infoMap[key];
info._pCallback = pCallback;
info._pUserData = pUserData;
}
void UpdateKeyPress(int key)
{
InfoMap::const_iterator itr = _infoMap.find( key );
if( itr != _infoMap.end() )
itr->second._pCallback( itr->second._pUserData );
}
};
void OnAccelerate(any_ptr pUserData)
{
// Expect the speed to be passed in
float *pSpeed = static_cast<float*>(pUserData);
assert( pSpeed );
// Increment speed
++(*pSpeed);
}
void OnDrinkPotion(any_ptr pUserData)
{
// Expect the health to be passed in
int *pHealth = static_cast<int*>(pUserData);
assert( pHealth );
// Increment health
++(*pHealth);
}
enum Keys
{
UpArrowKey,
EnterKey
};
int main(int /*argc*/, char * /*argv*/[])
{
KeyPressNotifier notifier;
float speed = 10.5f;
notifier.Register( UpArrowKey, OnAccelerate, &speed );
float health = 100;
notifier.Register( EnterKey, OnDrinkPotion, &health );
notifier.UpdateKeyPress( UpArrowKey );
notifier.UpdateKeyPress( EnterKey );
assert( speed == 11.5f );
assert( health == 101 );
}
当 OnDrinkPotion
函数的作者尝试通过 any_ptr
访问玩家的生命值,并假设它是整数时,转换会产生一个空指针。这会导致作者下一行的断言失败,提醒作者有问题。
将 any_ptr 与现有的回调函数一起使用
与前面的示例不同,现有的库代码不能总是自由修改以使用 any_ptr
而不是 void 指针。在这种情况下,与其传递指向所需数据的 void 指针,不如传递指向 any_ptr
的 void 指针,而该 any_ptr
指向所需数据。可以遵循一个约定,即回调函数中的用户数据 void 指针始终指向 any_ptr
。尽管(出于各种实际原因)有时可能需要对该约定有一些例外,但如果使用得当,它可以提高类型安全性。
限制
- 尽管这实际上不应该成为问题,但
any_ptr
比原始 void 指针占用的空间更大(其大小是一个 void 指针加上一个整数)。 - 从
any_ptr
进行类型转换比从 void 指针进行类型转换要慢。同样,这实际上也不应该成为问题。 - 从
any_ptr
转换为基类指针将不起作用。但是,这实际上是一个安全特性!从 void 指针转换为任何非回到完全相同的对象类型的指针,并不能保证得到一个有效对象指针(尤其是在多重继承的情况下)。
闭运算
any_ptr
源代码根据 MIT 许可发布,并在以下编译器上进行了测试
- Microsoft Visual C++ 2005/2008/2010
- GCC 4.4.1 (TDM-2 MinGW32)
还有很大的改进空间;如果您对代码进行了修改、改进,或者有一些更好的想法,我很想知道。您可以通过电子邮件 francisxavierjp [at] gmail [dot] com 与我联系。随时欢迎评论和建议!