Tomato & Egg 🍅🥚

Rewriting Data Structures Course Project 3

Date 2026-06-27    Tags programming, cpp, notes

Here’s the story: I’m taking a data structures course this semester. Although I don’t find it particularly difficult, Project 3 came with so many restrictive rules and constraints that it made me quite frustrated. So after submitting the required version, I took some extra time to rebuild a program from scratch that implements exactly the same functionality, but with no restrictions whatsoever—except for using C++17. In the end, I came up with an architecture that uses callback functions for decoupling. This post is a brief introduction to how I built this architecture.

Here is the source code for the rewritten version. Since the functionality is also described in the readme, I won’t go into too much detail here. In short, it’s a highly extensible species evolution simulation program.

Note: This post is a translated version. See here for the original content.

Pain Point: Complexity in the Simulator Layer

Due to the project requirements1, in the submitted code, the simulator layer had a huge switch statement block that directed the underlying engine to perform different physical operations based on the instruction type of the creature program. Below is a code snippet (not exactly the submitted code, but the functionality and structure are identical):

void Simulator::SimulateCreature(unsigned int creature_index) {
  ...
  current_instruction = engine_.get_instruction(creature_index);  // obtain current instruction
  ...
  switch (current_instruction.opcode) {
    case Opcode::kHop:  // hop forward
      engine_.MoveCreature(creature_index);
      break;
    case Opcode::kLeft:  // turn left
      engine_.TurnCreatureLeft(creature_index);
      break;
    ...
  }
  ...
}

Functionally, this works fine, but it has several drawbacks:

In fact, my local code quality tool directly warned that the function containing this switch block had excessive complexity. Fortunately, the online judge didn’t give me a deduction for that.

But why was instruction execution placed inside the simulator in the first place? Because the engine is managed by the simulator, and the execution of instructions depends on various physical operations provided by the engine (e.g., moving forward, turning left, infecting the creature ahead, etc.).

Association: Callback Functions Learned in Embedded Development

After I finished writing that function, I kept thinking about whether I could decouple instruction execution from the simulator.

One day while showering, I suddenly recalled callback functions learnt from embedded development. Callbacks are typically used with interrupts to handle specific events when they occur. The basic architecture taught by the teacher was:

đź’ˇ In embedded systems, interrupts are one way to handle events. When an event occurs, the corresponding device notifies the microcontroller (MCU), which then interrupts its current program to process the event.

This architecture allows the callback layer to be completely unaware of what the application layer does—it mechanically executes the callback functions passed to it. Thus, when requirements change, the callback layer does not need to be modified. This is a form of decoupling.

Solution: Dependency Inversion

In the architecture above, the callback layer does not depend on the concrete implementation of the application layer; instead, both depend on the function type of the callback. This is the Dependency Inversion Principle, which states:

High-level modules should not depend on low-level ones; both should depend on abstractions.

Abstractions should not depend on implementation details; implementation details should depend on abstractions.

In the example above, the function type of the callback is the abstraction. By extending this idea, the principle can also be applied to this project. Here’s what I did:

This callback list serves as the unified abstraction, and both the simulator layer and the engine layer depend on it. Now, in the simulator’s execution logic, we only need to locate the corresponding execution method by the instruction name, call its function-call operator, and pass in the prepared callback list and other relevant parameters. The logic becomes much simpler, and for extensions, we can simply inherit a new execution class.

void Simulator::SimulateCreature(unsigned int creature_index) {
  ...
  current_instruction = engine_.get_instruction(creature_index);  // obtain current instruction
  ...
  std::shared_ptr<IMethod> executor = get_executor(current_instruction.opcode);  // obtain executor
  ...
  (*executor)(world_callbacks, creature_index, ...);  // dispatch the executor
  ...
}

Although the rewritten SimulateCreature function may still look complex on the surface, its core is exactly the three lines above: fetch the instruction, find the executor, and execute with parameters. Everything else consists of checks and validations to satisfy various other requirements. If new features need to be added later, this function never needs to be touched.

Footnotes

  1. What’s the point of even whitelisting header files? They didn’t even allow stdexcept or memory, so I had to throw exceptions using string literals and couldn’t use smart pointers—very inelegant indeed.Â