动态转换到void指针有实际用途吗?

77
在C++中,T q = dynamic_cast<T>(p)这种写法对指针p进行运行时转换,将其转换为指向另一个指针类型T,而T必须位于*p的动态类型的继承层次结构中才能成功。这样做通常都是可以的。
然而,还可以执行dynamic_cast<void*>(p),它将简单地返回一个指向"最派生对象"(参见C++11中的5.2.7::7)的指针。我知道这个特性可能是在实现动态转换时免费提供的,但它在实践中有用吗?毕竟,它的返回类型最多为void*,那么这有什么好处呢?

8
翻译:仅是猜测,但这是否可以用于明确定义对象身份? - Björn Pollex
@BjörnPollex:但是p也会这样...是否存在这样的情况,即p1 == p2,但dynamic_cast<void*>(p1)!= dynamic_cast<void*>(p2) - Kerrek SB
8
我明白你的意思:我们可能会有 p1 != p2,但实际上它们指向相同的对象。我想如果我们有一个以 void * 为键的索引,那就有意义了。(不过 void 指针本身将无法使用。) - Kerrek SB
2
@BjörnPollex:你应该把那个评论转化成一个答案——这听起来像是一个合理的想法,值得发一篇帖子。 - Kerrek SB
糟糕,我的错 :) 没有考虑得够深入 - 感谢您的指点。 - John Humphreys
显示剩余4条评论
7个回答

73

dynamic_cast<void*>() 可以用于检查对象的身份,即使处理多重继承。

尝试以下代码:

#include <iostream>

class B {
public:
    virtual ~B() {}
};

class D1 : public B {
};

class D2 : public B {
};

class DD : public D1, public D2 {
};

namespace {
    bool eq(B* b1, B* b2) {
        return b1 == b2;
    }

    bool eqdc(B* b1, B *b2) {
        return dynamic_cast<void*>(b1) == dynamic_cast<void*>(b2);
    }
};

int
main() {
    DD *dd = new DD();
    D1 *d1 = dynamic_cast<D1*>(dd);
    D2 *d2 = dynamic_cast<D2*>(dd);

    std::cout << "eq: " << eq(d1, d2) << ", eqdc: " << eqdc(d1, d2) << "\n";
    return 0;
}

输出:

eq: 0, eqdc: 1

我将奖励授予这个问题,主要是因为我觉得其他答案没有提供任何深刻的新内容。这不是最初的意图,但我不想让赏金白白浪费。如果有人想出一个新的好答案(或编辑现有答案),我很乐意再次开始“奖励”赏金! - Kerrek SB

7
请记住,C++允许您以旧的C方式执行操作。
假设我有一些API,在其中我被迫通过类型void*走私对象指针,但最终传递给回调函数的将知道它的动态类型:
struct BaseClass {
    typedef void(*callback_type)(void*);
    virtual callback_type get_callback(void) = 0;
    virtual ~BaseClass() {}
};

struct ActualType: BaseClass {
    callback_type get_callback(void) { return my_callback; }

    static void my_callback(void *p) {
        ActualType *self = static_cast<ActualType*>(p);
        ...
    }
};

void register_callback(BaseClass *p) {
   // service.register_listener(p->get_callback(), p); // WRONG!
   service.register_listener(p->get_callback(), dynamic_cast<void*>(p));
}

这段错误的代码之所以出错是因为它在存在多继承时失败了(而且在不存在多继承时也不能保证正常工作)。

当然,这个API并不是非常符合C++风格,即使“正确”的代码在继承ActualType时也可能出错。因此,我不会认为这是一个 brilliant use of dynamic_cast<void*>,但它可以使用。


3
好的,但是 my_callback 可以使用 dynamic_cast<ActualType*>(static_cast<BaseClass*>(p)),对吧?换句话说,我们可以使用 BaseClass* 作为 C 封装的基础。 - Kerrek SB
@Kerrek:是的,我认为你可以这样做。就此而言,你可以添加另一个虚函数 BaseClass::get_this_for_callback,并让每个派生类完全控制如何打包指针。 - Steve Jessop
@SteveJessop:只是好奇,如果在my_callback中通过void*传回了p,那么在多重继承的情况下,如果将p转换为最派生类型作为register_listener的第二个参数是否有影响?我问这个问题是因为在my_callback内部,您正在执行一个static_cast到一个ActualType*,所以似乎无论是将指向最派生类型的指针还是指向基类的指针传递给my_callback都没有关系...无论哪种方式,它最终都会成为指向ActualType对象的指针,对吗? - Jason
@Jason:不行。在多重继承的情况下,并且假设某些基类不为空,则至少一个基类的地址与最终派生对象的地址不同。从void*ActualType*的静态转换仅在输入是ActualType对象的地址时才会产生正确的指针值,因此如果BaseClass恰好是位于不同地址的基类,则会出现错误。 - Steve Jessop
即使没有MI,从void到T的强制转换只有在值来自T到void时才是有效的。由Derived到Base到void到Derived的转换具有未定义的行为(可能在没有MI的情况下可能会工作)。 - curiousguy
显示剩余2条评论

4
将指针强制转换为void*自C语言时代以来就非常重要。最适合的位置是操作系统的内存管理器中,需要存储所有指针和所创建对象的信息。将其存储在void*中,可以将任何对象存储到内存管理器数据结构中,包括堆/ B+树或简单的arraylist
举个例子,创建一个包含完全不同类别的通用项(列表)只能使用void*实现。
标准规定,动态类型转换应返回null以防止非法类型转换,同时还保证除函数指针之外,任何指针都可以将其转换为void*并从其中转换回来。
在普通应用程序级别上,void*类型转换的实际用途很少,但在低级/嵌入式系统中广泛使用。
通常,您会想要使用reinterpret_cast进行底层操作,例如在8086中,它用于计算相同基地址的指针偏移量以获取地址,但不仅限于此。 编辑: 标准规定,您可以使用dynamic_cast<>将任何指针转换为void*,但它并未说明您不能将void*转换回对象。
对于大多数情况而言,它是单向的,但有一些不可避免的用途。
标准仅表示dynamic_cast<>需要类型信息才能将其转换回所请求的类型。
许多API需要您将void*传递给某个对象,例如java/Jni代码将对象作为void*传递。如果没有类型信息,就无法进行转换。如果您确信所请求的类型是正确的,可以使用一个技巧要求编译器执行dynmaic_cast<>请看下面的代码:
class Base_Class {public : virtual void dummy() { cout<<"Base\n";} };
class Derived_Class: public Base_Class { int a; public: void dummy() { cout<<"Derived\n";} };
class MostDerivedObject : public Derived_Class {int b; public: void dummy() { cout<<"Most\n";} };
class AnotherMostDerivedObject : public Derived_Class {int c; public: void dummy() { cout<<"AnotherMost\n";} };

int main () {
  try {
    Base_Class * ptr_a = new Derived_Class;
    Base_Class * ptr_b = new MostDerivedObject;
    Derived_Class * ptr_c,*ptr_d;

        ptr_c = dynamic_cast< Derived_Class *>(ptr_a);
        ptr_d = dynamic_cast< Derived_Class *>(ptr_b);

        void* testDerived = dynamic_cast<void*>(ptr_c);
        void* testMost = dynamic_cast<void*>(ptr_d);
        Base_Class* tptrDerived = dynamic_cast<Derived_Class*>(static_cast<Base_Class*>(testDerived));
        tptrDerived->dummy();
        Base_Class* tptrMost = dynamic_cast<Derived_Class*>(static_cast<Base_Class*>(testMost));
        tptrMost->dummy();
        //tptrMost = dynamic_cast<AnotherMostDerivedObject*>(static_cast<Base_Class*>(testMost));
        //tptrMost->dummy(); //fails

    } catch (exception& my_ex) {cout << "Exception: " << my_ex.what();}
    system("pause");
  return 0;
}

如果有任何不正确之处,请纠正我。


+1 你有几个相关链接吗?我对这种“低层次的东西”很感兴趣。 - Sim
这个链接包含了泛型数据结构的代码。http://code.google.com/p/c-generic-library/ - Praveen
2
根据C++标准第5.2.7/2节,你不能在void*上执行dynamic_cast操作......因此,在通用数据结构中使用dynamic_cast恢复void*的类型是行不通的,即使该void*通过dynamic_cast<void*>生成。执行转换的指针必须是指向完整类类型的指针。 - Jason
确实。恐怕这个答案有些偏离重点。我知道一般的空指针。然而,问题是关于对多态类指针进行dynamic_cast的。 - Kerrek SB
确实,dynamic_cast<> 是单行道,但如果您确定对象的类型信息,则可以从中获取对象。除此之外,在没有类型信息的情况下,它没有可移植性的用途,除了比较指针。但是,它作为语言功能存在,因为这是可能的,语言只能保证您使用它。 reinterpret_cast<> 也是如此。 - Praveen
@Praveen:非常感谢你的更新。希望你不要误解我的意思,但我仍然不相信这个答案解决了问题的多态性质。动态转换是一个非常复杂的操作,它会进行大量的工作并改变指针的实际数值。如果我的意思不明确,我可以尝试让问题更加精确。我的问题具体涉及使用dynamic-cast-to-void-pointer的多态用途。 - Kerrek SB

1

当我们将存储器放回内存池,但仅保留基类的指针时,这是非常有用的。在这种情况下,我们应该找出原始地址。


嗯...你能详细说明一下这种情况何时会有用吗? - Kerrek SB
现在我更明白了:您可以说p->~T();,然后通过一些提示的工厂(如T::create_inplace(dynamic_cast<void*>(copy_of_p)))进行放置新。 - Kerrek SB

1

在 @BruceAdi 的回答基础上,并受到 this discussion 的启发,这里有一个可能需要指针调整的多态情况。假设我们有这样一个工厂类型的设置:

struct Base { virtual ~Base() = default; /* ... */ };
struct Derived : Base { /* ... */ };

template <typename ...Args>
Base * Factory(Args &&... args)
{
    return ::new Derived(std::forward<Args>(args)...);
}

template <typename ...Args>
Base * InplaceFactory(void * location, Args &&... args)
{
    return ::new (location) Derived(std::forward<Args>(args)...);
}

现在我可以说:

Base * p = Factory();

但是我该如何手动清理它呢?我需要实际的内存地址来调用::operator delete

void * addr = dynamic_cast<void*>(p);

p->~Base();              // OK thanks to virtual destructor

// ::operator delete(p); // Error, wrong address!

::operator delete(addr); // OK

或者我可以重复使用内存:

void * addr = dynamic_cast<void*>(p);
p->~Base();
p = InplaceFactory(addr, "some", "arguments");

delete p;  // OK now

在调用析构函数后使用 dynamic_cast 可能不是一个好主意。 - Tadeusz Kopec for Ukraine
倒数第二个代码片段:析构函数,在动态转换的结果上使用::operator delete。幸运的是,这很容易修复。 - Tadeusz Kopec for Ukraine

0

这可能是通过ABI提供不透明指针的一种方式。 不透明指针 - 更一般地说,不透明数据类型 - 用于在库代码和客户端代码之间传递对象和其他资源,以使客户端代码可以与库的实现细节隔离开来。 当然,还有其他 方法可以实现这一点,也许其中一些对于特定的用例会更好。

Windows在其API中广泛使用不透明指针。 例如,HANDLE通常是指向您拥有HANDLE的实际资源的不透明指针。 HANDLE可以是内核对象,如文件、GDI对象和各种用户对象 - 所有这些对象在实现上必须大不相同,但都作为HANDLE返回给用户。

#include <iostream>
#include <string>
#include <iomanip>
using namespace std;


/*** LIBRARY.H ***/
namespace lib
{
    typedef void* MYHANDLE;

    void        ShowObject(MYHANDLE h);
    MYHANDLE    CreateObject();
    void        DestroyObject(MYHANDLE);
};

/*** CLIENT CODE ***/
int main()
{
    for( int i = 0; i < 25; ++i )
    {
        cout << "[" << setw(2) << i << "] :";
        lib::MYHANDLE h = lib::CreateObject();
        lib::ShowObject(h);
        lib::DestroyObject(h);
        cout << "\n";
    }
}

/*** LIBRARY.CPP ***/
namespace impl
{
    class Base { public: virtual ~Base() { cout << "[~Base]"; } };
    class Foo   : public Base { public: virtual ~Foo() { cout << "[~Foo]"; } };
    class Bar   : public Base { public: virtual ~Bar() { cout << "[~Bar]"; } };
};

lib::MYHANDLE lib::CreateObject()
{
    static bool init = false;
    if( !init )
    {
        srand((unsigned)time(0));
        init = true;
    }

    if( rand() % 2 )
        return static_cast<impl::Base*>(new impl::Foo);
    else
        return static_cast<impl::Base*>(new impl::Bar);
}

void lib::DestroyObject(lib::MYHANDLE h)
{
    delete static_cast<impl::Base*>(h);
}

void lib::ShowObject(lib::MYHANDLE h)
{
    impl::Foo* foo = dynamic_cast<impl::Foo*>(static_cast<impl::Base*>(h));
    impl::Bar* bar = dynamic_cast<impl::Bar*>(static_cast<impl::Base*>(h));

    if( foo ) 
        cout << "FOO";
    if( bar )
        cout << "BAR";
}

有趣 - 不过你确定 static_cast<Base*>(dynamic_cast<void*>(pointer_to_base)) 是正确的吗?例如,结果指针将无法再次动态转换为void指针,实际上它已经不能再使用了。 - Kerrek SB
@JohnDibling:我认为我用那个结构成功地导致了崩溃。想象一下:如果你有一个指向派生对象x的Base * p;,那么static_cast<Base*>(dynamic_cast<void*>(p))reinterpret_cast<Base*>(&x)相同,而不是static_cast<Base*>(&x) - Kerrek SB
这段代码存在许多问题:1)根据定义,dynamic_cast<void*> (new T) 意味着 new T。2)reinterpret_cast<T*> (handle) 应该是 static_cast<T*> (handle)handle 是一个 void*)。3)在将 impl::Foo*(或 impl::Bar*)转换为 void* 后,唯一可以做的不涉及未定义行为的事情就是将其转换回 impl::Foo*(或 impl::Bar*)。但你不能这样做,因为你甚至不知道你是否有一个 Foo 还是 Bar!4)我会编辑代码以使其正确。 - curiousguy
顺便说一句,我不知道编辑是否是提出代码更改的“正确”方式,但我不知道其他方法! - curiousguy
@curiousguy:如果您的编辑使我的代码更接近我想要的结果,我没有反对意见。不过要注意,很多人可能会感到非常冒犯。 - John Dibling
显示剩余12条评论

0

不要在家里这样做

struct Base {
    virtual ~Base ();
};

struct D : Base {};

Base *create () {
    D *p = new D;
    return p;
}

void *destroy1 (Base *b) {
    void *p = dynamic_cast<void*> (b);
    b->~Base ();
    return p;
}

void destroy2 (void *p) {
    operator delete (p);
}

int i = (destroy2 (destroy1 (create ())), i);

警告:如果D被定义为:

struct D : Base {
    void* operator new (size_t);
    void operator delete (void*);
};

那么这将不会起作用,并且没有办法使其工作。


@SteveJessop 好的,我已经放置了大的警告信息。 - curiousguy

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