Using Reflection in C++26 and a Toaster to Learn Reflection

How hard is it to put a Boeing 737 safely on the ground? I asked my toaster, and it said: extremely difficult.

Not something you would like to hear when the runway is already visible through the front windshield. Not much time left to learn the basics of avionics, cockpit instruments, procedures, and the rest.

Luckily, I used to write software for full-flight simulators — sophisticated machines used so future pilots don’t need to crash real planes while learning how to operate them.

All we need is reflection.

Okay, probably more. But from the whole codebase I worked with, reflection was the part I remember as the most difficult. The code was written by real C++ wizards and contained hundreds of lines of templates and macros, with the word META appearing almost every second line. All this magic just so we could access all the variables from the simulation engine.

If we can get reflection right, the rest of the task should be child’s play.

“Toaster, let’s talk about C++ and reflection.”

A quick crash course, and I was able to write down three lessons learned. What you see below is the version after multiple iterations, until I could finally prove to myself that my mind understood what was going on.

First lesson learned: reflection is just a mechanism for accessing the compiler’s knowledge about your code.

Second lesson learned: there is a cat ears* operator ^^ in C++. It returns a std::meta::info value that acts as our handle to the reflected stuff, and we can use a set of std::meta:: functions to get useful information from it. All of this happens at compile time.

Third lesson learned: the opposite direction of the cat ears operator is [: :]. I don’t have a name for it yet, but it looks quite toasterish.

“Toaster, am I missing anything on my list?”

Fourth lesson learned: reflection itself happens at compile time, but the code produced using it can operate on runtime objects. I thought I already understood this one, but the toaster insisted that I put it on the list anyway.

Closely related to the fourth lesson are annotations. Those are actually interesting because they are explicitly designed to attach information to declarations that reflection can later observe.

[[=cli_hidden{}]]
float internal_state;

Of course there was more, but the ground was getting closer, so there was no time for this. It was time to practice.

ctrl+T
tmux
claude

create a simple project (cmake) showing usage of reflections in cpp (^^operator). Class aircraft containing pitch, roll, yaw – float and landed – bool. All 4 should be exposed via CLI using reflection mechanism

Claude was “gesticulating” for more than 10 minutes. Luckily, our plane was a very slow one, so I could go make a coffee, take my dog out, and come back before it was done.

The generated code compiled. It worked. And it was perfect as a starting point for exploring the topic.

And that was enough — a small playground to mess with the code. What happens if I add this, remove that, or hit the whole thing with a hammer a few times? Just code-monkey playtime mode.

After the monkey finished playing, we were ready.


  template <typename T> constexpr auto members = std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()));

  // ...
    
  Aircraft ac;
  template for (constexpr auto m : members<Aircraft>) {
      if constexpr (std::meta::identifier_of(m) == "landed") {
          if constexpr (std::meta::type_of(m) == ^^bool) {
              ac.[:m:] = true;
          }
      }
  }

Touchdown.

* Finally! After all those years, we have something cool in our language. I was always jealous that Kotlin had its Elvis operator ?:

Author’s note: there was a time when I asked the Toaster for illustrations in the “Thinking Toasters” style and got nice, warm, happy drawings. Now I get neon pink fuchsia. I think the ghost of Mean Girl is here for good. See previous post.

The Secret Art of Keeping the Archwhale Alive

The Beast

There is a whale no one sees, circling slowly beneath the surface of every software project.

A mighty beast that carries systems on its back.

Be aware of its strength. When it is weakened or forgotten, it can pull the entire project down into the black depths of the entropy sea. And it does this so slowly, that by the time someone realizes what is happening, it is already too late. Planning turns to chaos, change becomes impossible, and there are no more doughnuts from the manager. People leave as the music fades into its final violins*. And the light goes out.

Flip the soundtrack

Things don’t need to end this way—if we simply give our archwhale what it craves most: attention.

And when I say “we,” I mean everyone involved in the project. Each of us adds a small piece to the story. Adding something means taking responsibility for it.

Now the most important part: to care about a whale is not to just think about it (even if your thoughts are warm, sophisticated, or reach far into the future).
To care about a whale is to take a knife and cut it into pieces**.

Chop chop chop?

Yes—but not so fast.

First, let’s clarify what this actually means.

As explained in this article, there are countless axes along which architecture can be sliced, depending on intent. Search long enough and you’ll find hundreds of possible artifacts: designs, diagrams, documents—plus frameworks and blog posts comparing architecture to whales, bridges, or chocolate cakes.

So our first problem isn’t a lack of options, but an excess of them.

We can’t just start creating projections at random. Too much documentation is as harmful as too little. Before we start running around with diagram-knives, we need to stop and ask a simple question:

What are we actually trying to achieve?

The spatial dimension

You carry the project vision inside your head. You navigate it effortlessly. You know where things are solid—and where shortcuts were taken just to keep things moving. You already plan new features, consider possible risks, and think about how to mitigate them.

What lives in your head is similar to what an author carries when writing a book: an entire universe where the real story unfolds. Just like you, the author can explore multiple possible futures happening inside.

Now imagine not one author, but a hundred, all writing the same book. Without synchronization, one kills the main character while another sends him to Scotland to find a brother who was never missing.

The universe must be shared.

That’s why we externalize it. Architecture artifacts—API contracts, dependency graphs, interface boundaries—are projections of the system that enable shared reasoning, coordination, and onboarding, keeping the universe stable while many minds shape it at once.

The time dimension

You carry the project vision inside your head.

Today.

Tomorrow your attention shifts. A month from now, you won’t remember why things are the way they are.

“It’s all in the code,” one might say. But that’s not true. Many decisions don’t affect how code is written, but how it is not written.

Why was language X chosen instead of Y?
Was market availability considered? Ecosystem maturity? Team experience?
And when a framework was selected, which trade-offs were accepted—and are they still valid?

What we want to record is not just why we chose A, but the full reasoning behind that choice.

In this sense, architecture artifacts are memory. We use them to keep the universe stable while time passes.

Not just records — thinking surfaces

Artifacts have one more important function: they act as thinking surfaces—places where ideas are tested before they harden into decisions.

You definitely know how this works. You don’t create class diagrams when classes already exist in code—you do it before, to see how dependencies might look. This allows to reason at a higher level of abstraction than the implementation.

The same applies to ADRs. Instead of writing an ADR after a choice is made, start earlier. Capture doubts, alternatives, and trade-offs. After execution, clean it up and keep it.

This suggests that artifacts should be created only when we actively work on a subject. In general, yes—but they should also be reviewed from time to time (for example, at each major release). Check whether they still carry information worth caring about. Outdated artifacts can be archived so they don’t introduce unnecessary noise.

Time for sushi

Now we are ready. We know what we want—and, more importantly, why. As in everything in the universe, balance matters. The number of produced artifacts must be just enough to keep the project synchronized across space and time. This way, it stays on the edge of exploration while remaining stable.

And remember: architecture survives only as long as people actively care for it.
Not admire it.
Not remember it fondly.

Care for it through small, deliberate acts: revisiting decisions, updating maps, removing what no longer matters, making the invisible visible again.

Ignore it, and it will not protest.
It will simply sink.

* Max Richter — “On the Nature of Daylight” fits perfectly
** Space archwhales love to be sliced — it keeps them alive.

Journey with Rust Part 4: First boss fight – fat pointer

Human asked: how can raw pointer be 16 bytes – that makes no sense. It should be just a normal pointer no?
Toaster thought for 20s and replied: Yeah, this is one of those “Rust is doing what?!” moments…

Intro

For a C++ programmer, learning Rust is as much fun as learning to ride a bicycle* – once you understand that assignment means move, everything starts rolling smoothly. Until one day when you encounter a Box inside a Box:

let inner: Box<dyn Debug> = Box::new(42);
let outer: Box<Box<dyn Debug>> = Box::new(inner);

You might think that winning a few battles against the compiler made you understand the language. Well, it didn’t. This is the moment you realize how much you don’t know, and that skipping all those pages of the user manual may not have been the best idea after all.

Congratulations: you’ve reached the point where the Rust journey starts to be really interesting… and dangerous. Now, let’s climb back inside the box.

The Simple View: Box as Dynamic Allocation

A Box is described as a way to store data on the heap – and for a long time that’s exactly how I treated it. Something like memory allocation (new in C++) combined with unique pointer in one single concept. Meaning this:

let boxed_int = Box::new(42);

Is equivalent to this:

auto ptr = std::make_unique<uint32_t>(42);

In both cases, you create an object that owns a pointer to a heap-allocated integer.

Simple.

But there is more in a Box…

Because Box can do more than simply allocate memory. What it stores depends on the type you put inside it. To keep things simple, let’s focus on one use case: dynamic polymorphism, aka trait objects in Rust.

We all know how this works in C++. Everyone has heard of the vtable (and if not, here’s a good explanation: vtable-and-vptr). Whenever a class uses virtual functions, the compiler generates a table of function pointers and places it somewhere in the binary. Each instance carries a hidden vptr pointing to that table. All invisible thanks to compiler magic.

Rust takes a slightly different approach. The vtable still exists, but the pointer to it does not live inside the object itself. Rust follows the “don’t pay for what you don’t use” principle: plain data stays plain and carries no hidden fields. As a result, when we use dynamic dispatch, Rust builds a special kind of pointer – a fat pointer – that contains both the data address and the vtable pointer. You can see this clearly if you inspect one:


And that explains why we sometimes end up with a Box inside a Box.

Because a Box<dyn Trait> is itself a fat pointer, and when we want to pass something that looks like a single thin pointer (for example to C code), we need to heap-allocate the inner trait object so the outer Box can remain thin. One Box holds the data; the other holds the fat pointer describing how to use it.

And that leads us straight to the next topic.

Fat pointers can be dangerous

Why? Because it’s very easy to accidentally destroy the metadata that makes them work.

Consider this code:

// Create trait object
let trait_object: Box<dyn Drinkable> = Box::new(Beer::new("IPC", 4.5));
println!("Size of trait_object: {}", std::mem::size_of_val(&trait_object));

// So far so good - we can drink our beer
trait_object.drink();

// Convert trait object to raw pointer
let beer_ptr = Box::into_raw(trait_object);
println!("Size of beer_ptr: {}", std::mem::size_of_val(&beer_ptr));

// Store the raw pointer as a void pointer (not good)
let c_ptr = beer_ptr as *mut ::std::os::raw::c_void;
println!("Size of c_ptr: {}", std::mem::size_of_val(&c_ptr));

// ... part below might sit megabytes of code away

// Cast the void pointer back to a trait object pointer (function expects thin pointer)
let bad_beer = unsafe { Box::from_raw(c_ptr as *mut Box<dyn Drinkable>) };
println!("Size of beer_ptr_2: {}", std::mem::size_of_val(&bad_beer));

bad_beer.drink();

Not good. Drinking last beer crashes the whole universe.

A Box<dyn Drinkable> is represented as a fat pointer(16 bytes on a 64-bit machine) that holds both a data pointer and a vtable pointer. When we call Box::into_raw, we get a raw pointer of type *mut dyn Drinkable which is still fat (16 bytes) and not just single memory address as one could expect.

The moment we cast it to *mut c_void, we throw away half of that information: the vtable pointer is gone, and only the data address remains. The compiler and Clippy are both fine with this – the cast is legal – but there is no magic that keeps the vtable ptr alive somewhere.

And when we later try to use that thin pointer as if it were still a fat one, very bad things happen.

Happy ending

There sits a big, fat lie in the example above. When we cast the C pointer back to a Box, we do this as if the original fat pointer had been wrapped inside a thin one – that’s why we cast to *mut Box.

The good news is that Rust will not let us cast directly to *mut dyn Drinkable. The compiler knows you can’t magically recreate a fat pointer out of 8 bytes (ask your toaster for std::mem::transmute if you want to see proper way to do this). In other words: Rust refuses to fabricate the missing vtable pointer. So we are partially saved.

Partially – because once everything “looks fine”, someone might decide that a Box inside a Box is one Box too many (“raw pointers are just pointers, right?”). One box removed, one universe destroyed.

The happy part? In 99% of real-world Rust code, nobody deals with these problems.
And if someone does… well, they knew what they signed up for.

Toaster last words

“Rust will protect you from yourself…
until you insist otherwise.
After that, it politely steps aside and lets physics handle the rest.”

Now that we’ve learned the secret art of shooting ourselves in the foot, we can ‘safely’ move on with our Rust adventure. The journey continues…

* ok – its like pedaling uphill on a bumpy road with ducks wandering in front of you every 10 seconds. No one ever said riding a bike was pure pleasure.