Creating Custom Events

Using Smart Pointers (std::unique_ptr, std::shared_ptr) with SDL Custom Events

Instead of void pointers, can I use std::shared_ptr or std::unique_ptr with data1/data2? How?

Abstract art representing computer programming

Using C++ smart pointers like std::unique_ptr and std::shared_ptr is highly recommended for managing dynamic memory safely, and you can integrate them with SDL's custom event system, although it requires some care because the SDL_UserEvent struct fundamentally uses raw void* pointers for data1 and data2.

You cannot change the struct itself, but you can manage what those void* pointers point to using smart pointers.

Using std::unique_ptr

std::unique_ptr represents exclusive ownership. This model fits well if you want to transfer ownership of some heap-allocated data from the event pusher to the event handler.

  1. Pushing the Event: Allocate your data using std::make_unique. To store it in the event, you must release ownership from the unique_ptr and store the resulting raw pointer in data1 or data2.
  2. Handling the Event: When you receive the event, static_cast the void* back to the correct raw pointer type. Immediately wrap this raw pointer back into a std::unique_ptr. This transfers ownership to the handler scope, ensuring the memory is automatically deleted when the handler's unique_ptr goes out of scope.

For example:

#include <SDL.h>
#include <memory> // Required for smart pointers
#include <iostream>
#include "UserEvents.h" // Assuming this defines MY_EVENT_TYPE

struct EventPayload {
  int value;
  std::string message;
};

// Function that pushes an event
void PushUniquePtrEvent(int val, const std::string& msg) {
  // Allocate data on the heap with unique_ptr
  auto payloadPtr =
    std::make_unique<EventPayload>(
      EventPayload{val, msg}
    );

  SDL_Event event;
  event.type = UserEvents::MY_EVENT_TYPE;
  event.user.code = 0;
  // Release ownership and store the raw pointer
  event.user.data1 = payloadPtr.release(); 
  event.user.data2 = nullptr;

  SDL_PushEvent(&event);
  // payloadPtr is now empty (nullptr)
}

// Function or part of the loop that handles events
void HandleEvent(SDL_Event& event) {
  if (event.type == UserEvents::MY_EVENT_TYPE) {
    // Cast void* back to the raw pointer type
    EventPayload* rawPtr = static_cast<EventPayload*>(
      event.user.data1
    );

    // Immediately take ownership with a unique_ptr
    std::unique_ptr<EventPayload> payloadOwner{rawPtr}; 

    // Now you can safely use the data via payloadOwner
    std::cout << "Handled event: value="
              << payloadOwner->value
              << ", msg='" << payloadOwner->message
              << "'\\n";

    // Memory is automatically deleted when
    // payloadOwner goes out of scope here.
  }
}

This approach is relatively clean and safe, provided the handler always correctly takes ownership. Failure to do so results in a memory leak.

Using std::shared_ptr

std::shared_ptr manages shared ownership through reference counting. Integrating this with void* is more complex because the void* itself doesn't participate in reference counting.

Method 1: Storing Raw Pointer (Less Recommended)

You could store the raw pointer obtained via shared_ptr::get(). However, this is dangerous. The reference count isn't incremented, and the original shared_ptr (and potentially others) must be kept alive externally until the event is processed. This negates many benefits of shared_ptr.

Method 2: Storing Pointer to shared_ptr (Safer but More Complex)

A safer way is to allocate the shared_ptr object itself on the heap and store a pointer to that shared_ptr in data1 or data2.

  1. Pushing the Event: Create your data with std::make_shared. Then, create a new std::shared_ptr on the heap that copies the original one, and store the pointer to this heap-allocated shared_ptr.
  2. Handling the Event: Retrieve the pointer to the heap-allocated shared_ptr. Dereference it to get a local shared_ptr copy (which correctly increments the reference count). Crucially, you must then manually delete the shared_ptr object that was allocated on the heap in the pusher.

For example:

#include <SDL.h>
#include <memory>
#include <iostream>
#include "UserEvents.h"

struct SharedPayload { int id; };

// Function that pushes an event
void PushSharedPtrEvent(int id) {
  // Create the managed data
  auto originalSharedPtr =
    std::make_shared<SharedPayload>(SharedPayload{id});

  // Allocate a shared_ptr *itself* on the heap
  auto* heapSharedPtr =
    new std::shared_ptr<SharedPayload>(originalSharedPtr); 

  SDL_Event event;
  event.type = UserEvents::MY_SHARED_EVENT_TYPE;
  event.user.code = 0;
  event.user.data1 = heapSharedPtr; // Store pointer to shared_ptr
  event.user.data2 = nullptr;

  SDL_PushEvent(&event);
  // originalSharedPtr still exists, holds one ref count
}

// Function or part of the loop that handles events
void HandleSharedEvent(SDL_Event& event) {
  if (event.type == UserEvents::MY_SHARED_EVENT_TYPE) {
    // Cast void* back to pointer to shared_ptr
    auto* heapSharedPtr =
      static_cast<std::shared_ptr<SharedPayload>*>(
        event.user.data1
      );

    // Copy the shared_ptr (increments ref count)
    std::shared_ptr<SharedPayload> dataOwner = *heapSharedPtr; 

    // CRITICAL: Delete the shared_ptr allocated on the heap
    delete heapSharedPtr; 

    // Now use the data via dataOwner
    std::cout << "Handled shared event: id="
              << dataOwner->id << "\\n";

    // The underlying SharedPayload is deleted only when
    // the last shared_ptr (dataOwner, originalSharedPtr, etc.)
    // is destroyed.
  }
}

This works but involves manual heap management of the shared_ptr object itself, which adds complexity.

Conclusion

Using std::unique_ptr::release() in the pusher and reconstructing a std::unique_ptr in the handler is often the most straightforward way to use smart pointers for transferring exclusive ownership via SDL events.

Using std::shared_ptr requires more intricate handling, typically involving allocating the shared_ptr object itself on the heap. Always ensure the handler correctly manages the lifetime of the retrieved pointer according to the chosen strategy.

Answers to questions are automatically generated and may not have been reviewed.

sdl2-promo.jpg
Part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 118 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free, Unlimited Access

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved