通过接受void*的C接口传递shared_ptr

8
我有一个使用SDL的C++项目,特别是SDL事件。我想像UI事件一样使用事件系统来处理传入的网络消息。我可以定义一个新的事件类型并附加一些任意数据(请参见this example)。如果我使用普通指针,这就是我要做的事情:
Uint32 message_event_type = SDL_RegisterEvents(1);

/* In the main event loop */
while (SDL_Poll(&evt)) {
    if (evt.type == message_event_type) {
         Message *msg = evt.user.data1;
         handle_message(msg);
    }
}

/* Networking code, possibly in another thread */
Message *msg = read_message_from_network();
SDL_Event evt;
evt.type = message_event_type;
evt.user.data1 = msg;
SDL_PostEvent(evt);

目前我一直在使用shared_ptr<Message>。消息对象在构建后是只读的,而且在处理过程中可能被用于许多地方,所以我认为使用shared_ptr比较合适。

我想在网络端和事件处理端都使用一个指向该消息的shared_ptr。如果我这样做:

// in networking code:
shared_ptr<Message> msg = ...
evt.user.data1 = msg.get();

// later, in event handling:
shared_ptr<Message> msg(evt.user.data1);

然后有两个独立的shared_ptrs,任何一个都可以在另一个仍在使用它时删除Message对象。我需要以某种方式通过SDL_UserEvent结构传递shared_ptr,该结构仅具有几个void *和int字段。
此外,请注意,SDL_PostEvent立即返回;事件本身被放入队列中。处理程序可能会在网络代码中的shared_ptr已经超出范围之后很久才从队列中弹出事件。因此,我不能传递局部shared_ptr的地址进行复制。当复制发生时,它很可能不再有效。
有没有人遇到过类似的问题并知道一个好的解决方案?

相关问题:https://dev59.com/sknSa4cB1Zd3GeqPO4g7 - Jeremy Friesner
为什么要使用shared_ptr,如果需要的话,为什么不让另一个类创建和删除它们并作为引用传递呢? - user2946316
1
将指向 shared_ptr 的指针传递,并在接收函数中进行值复制。例如:evt.user.data1 = &msg;shared_ptr<Message> msg = *reinterpret_cast<shared_ptr<Message>*>(evt.user.data1); - Jonathan Potter
@JonathanPotter:看起来这是一个不错的解决方案,当复制可以在事件发布函数返回之前完成时。但是它不能。事件必须存在于队列中,当它被弹出时,原始的shared_ptr可能已经消失了。 - Edmund
@Edmund:听起来你需要自己编写引用计数系统,而不是依赖于shared_ptr(尽管如果可以手动增加引用计数,如@EyasSH在下面建议的那样,则shared_from_this可能是一个选项)。 - Jonathan Potter
3个回答

6

使用 new 分配一个指向 shared_ptr 的指针。这将调用构造函数(增加引用计数),但对应的析构函数不会被调用,因此shared_ptr永远不会销毁它的共享内存。

然后,在相应的处理程序中,只需在复制 shared_ptr 后销毁对象,从而使其引用计数恢复正常。

这与通过消息队列传递任何其他非基本类型的方式完全相同。

typedef shared_ptr<Message> MessagePtr;

Uint32 message_event_type = SDL_RegisterEvents(1);

/* In the main event loop */
while (SDL_Poll(&evt)) {
    if (evt.type == message_event_type) {
         // Might need to cast data1 to (shared_ptr<Message> *)
         unique_ptr<MessagePtr> data (evt.user.data1);
         MessagePtr msg = *data;
         handle_message(msg);
    }
}

/* Networking code, possibly in another thread */
MessagePtr msg = read_message_from_network();
SDL_Event evt;
evt.type = message_event_type;
evt.user.data1 = new MessagePtr (msg); 
SDL_PostEvent(evt);

消息对象一旦构造完成就是只读的

我想指出,这对于多线程安全来说是很好的甚至是必要的。你可能想使用 shared_ptr<const Message>


1
我一直在努力弄清楚这是如何工作的。我不明白的是,使用“new”分配的shared_ptr对象是如何被释放的? - Ken
unique_ptr 在 if 语句结束时会自动释放它。或者(可能更清晰?)我可以使用原始指针,在 if 语句结束时调用 delete data。我发现依赖于 unique_ptrdelete 更容易编写正确的代码。 - QuestionC
谢谢,现在清楚了。但是我的应用程序需要多个实体有机会处理相同的事件,并且我不希望负载在第一个处理程序之后被删除。如果我将“unique_ptr”更改为“shared_ptr”,那么它们是否会在所有作用域之外被释放? - Ken
我对你需要的内容感到困惑。如果没有更多细节,可能以SO问题的形式提出,这个问题是无法回答的。一般来说,如果您想要5个实体来处理“消息”,那么您可以为每个实体将MessagePtr放入队列中,或者为每个实体都有一个不同的队列,并将MessagePtr放入每个队列中。OP需要将shared_ptr放入队列中,这对我来说非常不寻常,但将多个shared_ptr的副本放入队列中是正当的情况之一。 - QuestionC

5

使用std::enable_shared_from_this似乎是一个理想的选择。

struct Message: std::enable_shared_from_this<Message>
{
    …
};

evt.user.data1 = msg.get();

// this msg uses the same refcount as msg above
shared_ptr<Message> msg = evt.user.data1.shared_from_this();

这只适用于C++11及以上版本,但它是一种优雅且易于维护的解决方案。 - Nicholas Smith
2
@qexyn std::shared_ptr C++11 及以上版本的特性。 - StenSoft
1
由于某种原因,我在考虑boost::shared_ptr。实际上,OP没有指定命名空间,但可能是std,在这种情况下,您的C++11解决方案非常适合。 - Nicholas Smith
1
当传递到C语言环境并在退出时减少引用计数时,您可能还希望手动增加引用计数。我认为在作为侵入式指针使用的shared_ptr中,这是可能的。在boost侵入式指针中肯定是可能的。 - EyasSH
好的,最终弄清楚了这是怎么工作的。第一个msg指针把消息放入队列后,什么阻止该消息在其作用域结束时被销毁? - Edmund
@Edmund 没有。如果您需要,请传递 shared_ptr 的实例。 - StenSoft

1

我想到了另一种潜在的技术:使用placement new将shared_ptr存储在与C结构相同的空间中:

SDL_Event evt;
evt.type = event_type;
// create new shared_ptr, in the same memory as evt.user.code
new (&evt.user.code) shared_ptr<Message>(msg);
SDL_PushEvent(&evt);

SDL然后将事件复制为C对象,直到稍后的代码从事件中提取消息。
shared_ptr<Message> get_message(SDL_Event& evt) {
    // copy shared_ptr out of evt
    shared_ptr<Message> msg = *reinterpret_cast<shared_ptr<Message> *>(&evt.user.code);
    // destroy shared_ptr inside the event struct
    (reinterpret_cast<shared_ptr<Message> *>(&evt.user.code))->~shared_ptr();
    return msg;
}

事件结构体中有几个字段应该足够容纳 shared_ptr(请参见https://github.com/spurious/SDL-mirror/blob/master/include/SDL_events.h#L485)。

我知道这有点 hacky。我希望对这种技术进行一些合理性检查。


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