2025-184

15:40:29, atom feed.

When I went to implement basic player movement, I needed to capture keyboard input. This is done in SDL by polling for events and then processing keyboard-related ones.

SDL_Event e;
while (SDL_PollEvent(&e)) {
  switch (e.type) {
    case SDL_EVENT_KEY_UP:
      // Fall through.
    case SDL_EVENT_KEY_DOWN: {
        if (event.key.key == SDLK_DOWN) {
          // ...
        }
      }
      break;
    default:
      break;
  }
}

What I want to focus on in this post is the implementation of SDL_Event. If you look at its documentation, you'll see that it's a union. Its first member is type which encodes the event type--it's what we're switching on in the example above. If you look at the implementation of any of the SDL_* union members, you will see the first field of each of those structs is an instance of SDL_EventType. SDL_EventType is implemented as a C enum, which is represented as an int behind the scenes, which is the same number of bits (although different semantics) as the Uint32 (uint32_t-equivalent) type member in the SDL_Event union on nearly all modern platforms.

For reference, see the implementations of, for example, SDL_ClipboardEvent, SDL_TouchFingerEvent, SDL_MouseButtonEvent, or SDL_QuitEvent.

In other words, no matter which member of the union you work with, the first field will always be something encoding the event type.

This construct is called a discriminated union (or tagged union). Here is an all-in-one example:

enum TypeTag { kFoo, kBar, kNone };
struct Foo {
  TypeTag tag;
  // ...
};
struct Bar {
  TypeTag tag;
  // ...
};
union DiscriminatedUnion {
  TypeTag tag;
  Foo foo;
  Bar bar;
};

With discriminated unions, you can avoid inheritance and still compose one type that behaves as an instance of this or that type depending on some context, and you just check the value of tag to know how to access fields of the union. You lose anything you might have otherwise gained through polymorphism, and the total size of DiscriminatedUnion is the size of its largest member (plus padding, potentially; see the interesting note at the bottom of the SDL_Event implementation). However, the discriminated union construct supports data-oriented design by concentrating data of interest nearby in memory instead of potentially scattered about through a network of pointers (whether these pointers be pointers-to-base-class in some data structure, or members of instances pointing to this and that data, or the CPU figuring out which methods to call via dynamic dispatch).