65.9K
CodeProject 正在变化。 阅读更多。
Home

类型安全的通用指针

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (18投票s)

2011 年 2 月 18 日

MIT

4分钟阅读

viewsIcon

67743

downloadIcon

419

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 正确性不兼容的类型转换将产生一个空指针。

示例 1
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
示例 2
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 与我联系。随时欢迎评论和建议!

参考文献

  1. 回调(计算机科学),来自维基百科
  2. 智能指针,来自维基百科
  3. Boost.Any,来自 Boost 库文档
  4. Const 正确性,来自 C++ FAQ Lite
  5. 异常处理,来自维基百科
  6. 运行时类型信息,来自维基百科
© . All rights reserved.