8

I have a C++ project that uses SDL, in particular SDL Events. I would like to use the event system for incoming network messages just as it is used for UI events. I can define a new event type and attach some arbitrary data (see this example). This is what I would do if I was using ordinary pointers:

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);

Instead, I've been using shared_ptr<Message> up to now. Messages are read-only objects once constructed, and might be used in lots of places while being handled, so I thought to use shared_ptr for them.

I would like to use a shared_ptr to the message in the network side, and also on the event handling side. If I do this:

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

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

then there are two independent shared_ptrs and either one could delete the Message object while the either is still using it. I would need to somehow pass the shared_ptr through the SDL_UserEvent struct, which only has a couple of void * and int fields.

Additional. Note that SDL_PostEvent returns immediately; the event itself is put on a queue. The event may be popped from the queue by the handler well after a shared_ptr to the message has gone out of scope in the networking code. So I can't pass the address of a local shared_ptr to copy from. By the time the copying occurs, it's likely to no longer be valid.

Has anyone faced a similar problem and knows of a nice solution?

Edmund
  • 10,533
  • 3
  • 39
  • 57
  • Related question: http://stackoverflow.com/questions/1482806/manually-incrementing-and-decrementing-a-boostshared-ptr – Jeremy Friesner Jun 26 '15 at 00:43
  • Why shared_ptr, why not let another class create and delete them and pass them as reference if needed? –  Jun 26 '15 at 00:43
  • 1
    Pass a pointer to the `shared_ptr`, and make a value copy of it in the receiving function. E.g. `evt.user.data1 = &msg;` and `shared_ptr msg = *reinterpret_cast*>(evt.user.data1);`. – Jonathan Potter Jun 26 '15 at 00:46
  • @JonathanPotter: looks like a good solution for when the copy can be made before the event posting function returns. But it can't. The event has to live on a queue, and when it gets popped the original shared_ptr to it may well have gone. – Edmund Jun 26 '15 at 02:53
  • @Edmund: It sounds like you need to roll your own reference counting system rather than relying on `shared_ptr` to me (although if the ref count can be manually incremented as @EyasSH suggests below then shared_from_this could be an option) – Jonathan Potter Jun 26 '15 at 03:02

3 Answers3

6

Allocate a pointer to the shared ptr with new. This calls the constructor (incrementing the reference count), but the corresponding destructor is not called so the shared_ptr will never destruct it's shared memory.

Then in the corresponding handler, just destroy the object after copying the shared_ptr bringing its reference count back to normal.

This is identical to how you'd pass any other non-primitive type through a message queue.

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);

Messages are read-only objects once constructed

I just want to point out, this is good and even necessary for the multithreading to be safe. You might want to use shared_ptr<const Message>

QuestionC
  • 10,006
  • 4
  • 26
  • 44
  • 1
    I've been trying to figure out how this works. What I don't understand is how the shared_ptr object that gets allocated with 'new' ever gets deallocated? – Ken Nov 25 '16 at 20:11
  • `unique_ptr` deallocates it at the end of the if statement. Alternatively (perhaps more clearly?) I could instead use a raw pointer and call `delete data` at the end of the if statement. I find it's easier to write correct code relying on `unique_ptr` than `delete`. – QuestionC Nov 25 '16 at 22:18
  • Thanks, that's clear now. But my application requires that several entities get an opportunity to handle the same event and I don't want the payload deleted after 1st handler. If I change the `unique_ptr` to be a `shared_ptr`, will that deallocate once they're all out scope? – Ken Nov 26 '16 at 09:48
  • I'm confused about what exactly you need here. I don't think the problem's answerable without more details, probably in the form of a SO question. In general if you want 5 entities to handle the `Message`, then you either put the `MessagePtr` in the queue once for each entity, or you have a different queue for each entity and put the `MessagePtr` in each queue. OP's need to put a `shared_ptr` in the queue was very unusual to me, but sticking multiple copies of the `shared_ptr` in the queue is one case that justifies this choice. – QuestionC Nov 28 '16 at 15:52
5

Seems like ideal place to use 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();
StenSoft
  • 9,369
  • 25
  • 30
  • This will work only with C++ 11 and up, but it is an elegant and maintainable solution. – Nicholas Smith Jun 26 '15 at 01:19
  • 2
    @qexyn `std::shared_ptr` *is* C++11 and up – StenSoft Jun 26 '15 at 01:38
  • 1
    For some reason I was thinking boost::shared_ptr. Actually the OP does not specify namespace, but probably is std, in which case your C++11 solution is perfectly suited. – Nicholas Smith Jun 26 '15 at 01:48
  • 1
    You probably also want to manually increment the reference count when passing to C-land and decrementing it on the way out. I think this is possible in shared_ptr when used as an intrusive pointer. Certainly possible in boost intrusive pointers. – EyasSH Jun 26 '15 at 02:56
  • Ok finally figured out how this works. What stops the message from being destroyed when the first `msg` pointer goes out of scope after putting it on the queue? – Edmund Jul 22 '15 at 03:55
  • @Edmund Nothing. If you need that, you need to pass instance of `shared_ptr` instead. – StenSoft Jul 22 '15 at 11:56
1

I thought of another potential technique: using placement new to store the shared_ptr in the same space as the C struct:

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 then copies the event around as a C object, until later code extracts the message from the event:

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;
}

There are several fields inside the event struct that should be enough space for the shared_ptr (see https://github.com/spurious/SDL-mirror/blob/master/include/SDL_events.h#L485).

I'm aware that this is a bit hacky. I'd appreciate some sanity checking of the technique.

Edmund
  • 10,533
  • 3
  • 39
  • 57