Creating Custom Events

Managing Data Lifetime for SDL Custom Events

How do I manage the lifetime of data pointed to by data1/data2 if the event might be processed much later?

Abstract art representing computer programming

This is a crucial aspect of using custom events with data pointers. Since SDL_PushEvent only copies the SDL_Event structure itself (including the void* values in data1/data2), and not the data they point to, you must ensure that the pointed-to memory remains valid until the event is polled and processed by its handler. If the original data is destroyed before the event is handled, the data1/data2 pointers become dangling pointers, leading to undefined behavior when the handler tries to access them.

Here are the primary strategies for managing data lifetime:

1. Pointing to Long-Lived Objects

If the data associated with the event is part of an object that is guaranteed to exist for the duration of the event's lifecycle (from push to handling), you can simply store a pointer to that data.

Examples: Global variables, static variables, members of singleton manager classes, members of objects that persist for the entire application or game state duration (like the Button members in the lesson example).

// Button.h (Simplified from lesson)
class Button {
private:
  // This data lives as long as the Button instance
  MyConfig buttonConfig{ /* ... */ };
  int buttonId{ 123 };

public:
  void OnClick() {
    SDL_Event event;
    event.type = UserEvents::BUTTON_CLICKED;
    // Point directly to member data
    event.user.data1 = &buttonConfig; 
    // Use 'code' for simple integers if suitable
    event.user.code = buttonId; 
    event.user.data2 = nullptr;
    SDL_PushEvent(&event);
  }
};

// Assuming the Button object itself persists long enough
  • Pros: Simple, no dynamic allocation needed.
  • Cons: Only works if the data source naturally has a sufficiently long lifetime. Relies on careful design to ensure the source object isn't destroyed prematurely.

2. Heap Allocation and Ownership Transfer

Allocate the data dynamically on the heap using new (or preferably std::make_unique). The event pusher creates the data and passes the raw pointer via data1 or data2. Crucially, the event handler then becomes responsible for deleting the data using delete (or managing it via a smart pointer).

#include <memory> // For std::unique_ptr

struct ClickData { /* ... */ };

// Pusher Function
void PushClickEvent() {
  // Allocate on heap
  ClickData* dataPtr = new ClickData{ /* ... */ }; 

  SDL_Event event;
  event.type = UserEvents::ITEM_CLICKED;
  event.user.data1 = dataPtr; // Pass raw pointer
  event.user.data2 = nullptr;
  SDL_PushEvent(&event);
}

// Handler Function / Loop
void HandleEvent(SDL_Event& event) {
  if (event.type == UserEvents::ITEM_CLICKED) {
    ClickData* dataPtr = static_cast<ClickData*>(
      event.user.data1
    );

    // Use dataPtr...
    std::cout << "Processing item click...\\n";

    // CRITICAL: Handler must delete the data
    delete dataPtr; 
  }
}

Using std::unique_ptr makes this safer, as covered in the FAQ entry "Using Smart Pointers (std::unique_ptr, std::shared_ptr) with SDL Custom Events". The pusher uses release() and the handler reconstructs a unique_ptr to manage deletion automatically.

  • Pros: Flexible, works for data with any lifetime requirement. Clear transfer of ownership (especially with unique_ptr).
  • Cons: Requires careful manual memory management (new/delete) or correct use of smart pointers by the handler to avoid leaks or double deletions.

3. Shared Ownership (std::shared_ptr)

If the data needs to be accessed by multiple parts of the system (including potentially the original pusher after pushing the event) and its lifetime isn't tied to a single owner, std::shared_ptr is often the best solution.

As detailed in the smart pointer FAQ, the typical way to use this with SDL events involves allocating the shared_ptr object itself on the heap, storing a pointer to it in the event, and having the handler retrieve it, copy it (incrementing the reference count), and delete the heap-allocated shared_ptr wrapper. The underlying data survives as long as any shared_ptr instance refers to it.

  • Pros: Robust lifetime management for shared data. Data persists as long as needed.
  • Cons: More complex implementation than the other methods due to the interaction with void*. Introduces reference counting overhead.

Choosing a Strategy

The best approach depends on your specific needs:

  • Use long-lived objects when data is naturally persistent.
  • Use heap allocation with unique_ptr for transferring exclusive ownership of temporary data to the handler.
  • Use heap allocation with shared_ptr (via the pointer-to-shared-ptr method) when data lifetime needs to be shared between the pusher, handler, and potentially others.

We delve deeper into advanced memory management patterns and techniques for handling asynchronous data communication later in the course, exploring more sophisticated solutions beyond these fundamental approaches.

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