How It All Began
For over 10 years, C++ has had constexpr, which allows a programmer to cleverly offload some computations to the compiler. At the time, this blew my mind—the compiler could calculate some fairly complex things even before the program starts!
At some point, I thought: if the compiler can calculate everything for you, then why do you need a runtime at all? What are you going to do there—print the answer? What nonsense. That’s unsportsmanlike.
And that’s when my challenge was born:
“Hands-off” or “don’t even think about running the EXE file”
Setting the Task
To be clear, let’s state the main idea again: I want to write a program and build it with a compiler. The assumption is that all computations will be done at compile time. I don’t want to run the program. But the code must work. A contradiction in terms? Perhaps. But it has to be done.

To make things harder: I’m a Windows-centric programmer. We can witness the flame wars on this topic in the comments. For me, this means we’ll be dealing with Windows-specific entities: the Visual Studio compiler, cmd, bat, exe, and all of Linus’s other nightmares. This will also throw some additional difficulties our way, but I’m all for it.
The program must do something sensible and useful. We’ll dismiss the tired old examples of calculating factorials or Fibonacci numbers as trite, carrying no useful payload, and not posing a challenge. I probably won’t make the compiler play Doom either. And it’s not so much that the compiler can’t handle it—it’s more that I can’t handle it.
In the end, I decided to settle on something in between—we will indeed make the compiler play, but something significantly simpler. For example, Conway’s Game of Life. After all, if Google can afford to run this simulation on its search page, why can’t I make a C++ compiler do the same?
For those who didn’t follow the links but are unfamiliar with this “game,” I’ll leave an animation here:

Let’s refresh our memory of the rules of the Game of Life:
If a cell is alive and has fewer than two neighbors, it dies.
If a cell is alive and has more than three neighbors, it dies.
If a cell is empty but has three neighbors, a new cell is born.
And so on, generation after generation, to infinity.
“But waaaait,” you’ll say, “the Game of Life is a game. It should have frames, a main loop, infinite execution, rendering, for crying out loud, and you’re feeding us some nonsense about compile-only.” That’s partly fair, but in my challenge, I’m leaving myself a lot of room to maneuver, since the only thing that’s not allowed is running the executable file (neither directly nor indirectly). Otherwise, we can do anything our imagination allows. And here, we’ll have to show our full potential.
The Nuances of constexpr
Let’s get the language subtleties out of the way. C++ allows you to mark a function or method as constexpr. But this gives you no guarantees of compile-time execution. If non-constexpr parameters are passed to the function, your constexpr tag will be quietly ignored, and you won’t even know about it.
Look, this code will perform the calculation at compile time:
constexpr int twice(int n) { return n * 2; } int main() { return twice(17); }
The program will exit with code 34, and 17 * 2 will be calculated during the program’s build phase, with just the result 34 being written to the binary.
We’re talking about things in a vacuum right now, so let’s ignore the fact that the compiler can fold such a trivial multiplication on its own, even without the mention of
constexpr. For the sake of simplicity in our theorizing, let’s imagine that the compiler wouldn’t do this withoutconstexpr.
Now let’s make it so the program cannot be evaluated at compile-time:
constexpr int twice(int n) { return n * 2; } int main() { int n; std::cin >> n; return twice(n); }
Now the return code depends on user input, and the compiler’s authority to calculate the result at compile-time is—gone. Nevertheless, this code compiles and runs. That is, the twice function can be legally used in both compile-time and runtime contexts.
However, this doesn’t mean you can in principle smear constexpr on any function just in case. If you add something incompatible with compile-time only to the function, you get a compilation error:
constexpr int twice(int n) { std::cout << n * 2 << std::endl; return n * 2; }
When we try to build it, we get:
error C3615: constexpr function 'twice' cannot result in a constant expression
Okay, but what if I want to guarantee that my function always, always executes only in a compile-time context? For this, there is, for example, the std::is_constant_evaluated check from C++20. It’s a constexpr function that can be run inside another constexpr function to check if we are actually in a constexpr context right now or if we’ve been downgraded to runtime. You get the idea—the language standardization committee loves complexity.
In our case, we must guarantee that we are working in a compile-time context. Any deviation from this course should preferably be accompanied by a compilation error. And yes, we could smear constexpr and std::is_constant_evaluated everywhere, but that same standardization committee gave us a new specifier in C++20, consteval, which is essentially constexpr on steroids. You can use it to mark a method that is intended exclusively for execution by the compiler. Therefore, any suspicion that the method is being used at runtime will result in a righteous compilation error. And that’s what we need—strict guarantees that the compiler’s executable will be doing the sweating, not our executable.
This article will use C++20 features, and C++23 if needed. Yes, not everyone will be able to use these wonders in production, but what can you do—such is the life of a C++ developer.
Starting to Write Game of Life
Let’s just start writing code first, without worrying about consteval and the like. We’ll figure it out as we go.
So, we need a canvas of a certain size. I’m choosing 16x16. Later you’ll understand why this specific size, and you’ll appreciate the cleverness.
The abnormal programming starts here:
#include <array> constexpr bool _ = false; constexpr bool X = true; constexpr size_t N = 16; using Canvas = std::array<std::array<bool, N>, N>; constexpr Canvas life {{ {_,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,}, }}; int main() { return life[0][1]; }
I said I wouldn’t worry about compile-time yet, and here I am smearing constexpr all over everything. Well, sorry, I couldn’t resist. There are no calculations yet, but I’ve already laid the groundwork.
What we’ve done here: we introduced a two-dimensional canvas entity Canvas and declared the game’s starting frame life, on which a curious figure, the Glider, is placed. In the Game of Life, it behaves like a rotating, moving object that more or less maintains its shape over a certain period. It’s easier to show:

With a figure like this, it will be easy to check if the game logic is written correctly—the figure should move diagonally down and to the right with each frame.
I foresee a question that some might have: why did I write return life[0][1];? Without this line, the compiler would decide that all our variables were declared in vain and would just cut everything out of the binary.
Also, at address [0][1] there’s a live cell, so the program should return 1 upon completion. The thing is, it will never actually run, lol—let’s not forget the main rule of the challenge!
But really, we have nothing to run yet, since we haven’t even compiled.
Running the Compiler
Remember—we’re on Windows, and the most logical compiler available to us is the one used by Visual Studio—Microsoft Visual C++ or MSVC. As a binary, it’s known as cl.exe.
We don’t want to use Visual Studio itself as an IDE—that’s for the weak-spirited. We’re interested in using cl.exe directly. And here the first little nuance pops up. In Linux, I can just open a terminal and immediately run gcc.exe for my dirty deeds. But if you type cl /help in a freshly opened cmd, you’ll get:
'cl' is not recognized as an internal or external command, operable program or batch file.
By default, the shell environment is not configured to work with cl out of the box, and the path to it isn’t even in PATH. And this is by design. Here’s the extensive documentation on how to simply run the damn cl.
TL;DR: to touch the visual compiler with your bare hands, you first need to set up the shell environment, and you can do this in almost a dozen ways depending on the desired bitness and platform. There’s a separate .bat file for each environment option. The first rule of compiler usage hygiene in Windows: first run the environment batch file, then use cl.
This is how it works:
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" cl /help
Here we used the batch file for x64-native tools, which suits us.
You know all those x64/x86 Native Tools Command Prompt for VS2022 terminals? They are essentially pre-configured shells where cl.exe can be run right out of the box. We’re dismissing that option too, because we want to run it from any bare-bones terminal.
At this stage, the following command works for compilation:
cl /std:c++latest main.cpp
Its execution shows that our template compiles successfully, so we can continue writing the code.
Wrangling it into Compile-time
So, with our compiler unsheathed and ready, we continue to painstakingly craft the Game of Life.
In reality, the entire computational part of our simulation is painfully primitive: you take the old Canvas, calculate the new Canvas from it according to the game’s rules, somehow draw/display it, and repeat this indefinitely—that’s the whole program. The cornerstone of this algorithm will be a function with the signature Canvas update(Canvas old), which calculates the next generation of cells:
consteval Canvas update(Canvas old) { Canvas res; for (int r = 0; r < N; ++r) { for (int c = 0; c < N; ++c) { int neighboursCount = 0; for (int nr = r - 1; nr <= r + 1; ++nr) { for (int nc = c - 1; nc <= c + 1; ++nc) { if (nr == r && nc == c) continue; int wrappedR = (nr + N) % N; int wrappedC = (nc + N) % N; neighboursCount += static_cast<int>(old[wrappedR][wrappedC]); } } const bool isAlive = old[r][c]; res[r][c] = neighboursCount == 3 || (isAlive && neighboursCount == 2); } } return res; }
In fact, the entire essence and logic of the game fits into the line res[r][c] = neighboursCount == 3 || (isAlive && neighboursCount == 2);. The rest of the code is for scanning the cells and their neighbors.
Note that I’ve made the canvas wrap around. That is, if a live cell “goes off” the right side of the screen, it appears on the left side. This is done so that the life boiling on a small canvas is less likely to degenerate into nothing and can sustain itself for as long as possible.
I sneakily added consteval to the function, and the compiler didn’t even complain. Yes, it’s that simple—it agrees to calculate this at compile-time! Loops, branches, creating new variables—all these luxuries are available to us. The toothless constexpr from the distant C++11 couldn’t handle loops or local variables, required a single return statement, and only allowed branching with the ternary operator. Remembering all that, I’m impressed by the current capabilities of compile-time computations in the language.
Now let’s create a variable that will hold the second generation of cells:
constexpr Canvas newLife = update(life);
And our trick of using this variable so the compiler doesn’t cut everything to hell:
int main() { return newLife[0][0]; }
Theoretically, we now have the second frame of the game calculated at compile time. All that’s left is to figure out how to see it.
Trying to Extract Information
How can we get the result after the compiler finishes its work? The first thing that came to my mind was to make the compiler generate an asm listing in addition to the binary and look in there. Our calculated second frame should definitely be there as a constant. Well, we could also parse the EXE file—that constant should be there in some form too, but parsing a binary file with a complex structure smells of madness.
Assembler
To get an asm file, you need to pass the /Fa flag to the compiler. To keep the file from being too bloated, we’ll also try to apply the maximum optimization level, crossing our fingers that the optimizer doesn’t chop up our desired frame too much. For optimization, we’ll add the /O2 flag. So, we run the command:
cl /std:c++latest /Fa /O2 main.cpp
Now, in addition to main.exe, we get a main.asm file. Let’s look inside, and there, before the actual assembly opcodes, is a section with constant data declarations, where we see our familiar newLife as a set of 256 numbers. Here’s a snippet of the listing:
CONST SEGMENT ?newLife@@3V?$array@V?$array@_N$0BA@@std@@$0BA@@std@@B DB 00H ; newLife DB 00H ... DB 00H DB 01H DB 00H DB 01H DB 00H ... DB 00H DB 01H DB 01H DB 00H DB 00H ... DB 00H DB 00H DB 00H DB 00H DB 01H DB 00H ...
I copied this data array and did some magic on it in Notepad to make the frame readable:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
So, does it look like the second frame of a Glider crawling diagonally? It seems so! I pulled the same stunt for the third and fourth frames:
constexpr Canvas third = update(update(life)); // and constexpr Canvas fourth = update(update(update(life)));
I got the following “images”:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Doubts vanished—it’s our Glider, which means the Game of Life frames are being calculated correctly.
The Binary File
Now let’s try to stick our hands into the binary file and find the same data we have in the asm listing.
To do this, we’ll need at least a superficial understanding of the EXE file structure. I didn’t have this knowledge, but luckily I knew where I could easily and painlessly acquire it—I remembered that Alek OS on YouTube had a video where he explained the format of a Windows binary. By the way, I highly recommend Alek to everyone—his videos have filled more than one gap in my general knowledge that would be useful to any programmer.
I won’t get into the weeds, but I’ll explain in simple terms how I learned to jump to the places I need in the binary for my task:
Our frame’s array should be sought in the .rdata section of the binary. This is where all the read-only data resides.
The header that explains where to find the .rdata section is located almost at the beginning of the file:

In the image, we see the beginning of the EXE file in a hex editor. The yellow part is 8 bytes allocated for the .rdata label. 12 bytes after the yellow area, we find information about the start of the .rdata section in the file; it’s highlighted in red. This is little-endian, so from 00 F0 00 00 we get the offset 0xF000.
Let’s go straight there and take a look:

Hmm, I don’t see anything that looks like our array. In desperation, I scroll down a bit:

And there it is! Our Glider made of smiley faces is visible almost immediately; it’s hard to mistake it for anything else in this pile of binary noise. Far3, which I’m using to view the guts of the EXE, displays the binary file in 16-byte rows, and voilà—our 16x16 matrix is perfectly readable even in the raw binary!
I couldn’t figure out what specific data in the .rdata section lies before our array. I also didn’t understand the principle by which our array was placed in this particular spot in the .rdata section. Most likely, it was just put there, and that’s that.
However, I discovered that no matter how you compile, the array is always placed at the 0x320-th byte from the beginning of the .rdata section (or the 0xF320-th byte from the beginning of the file). Yes, this magic number depends on the phase of the moon and the compiler’s mood. But as long as my array is there, I can parse the binary and extract the result of the constexpr program’s calculation!
A parsed binary is cooler than a parsed asm text, so we’ll stick with extracting information from it.
How We’ll Bring the Game to Life
And now, watch my hands closely; the abnormal programming continues here.
Let’s copy the body of our life array into a file named life.txt:
{_,X,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,X,_,_,_,_,_,_,_,_,_,_,_,_,_}, {X,X,X,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_,_,_},
And in the C++ code, we’ll make an include worthy of hackers:
constexpr Canvas life {{ #include "life.txt" }};
From now on, let’s consider the life.txt file to be our “render window.” And, as you may have noticed, this file is also simultaneously a part of the C++ code.
Our cunning plan is as follows:
Compile the program
Parse the EXE for the new Game of Life frame
Write the new frame back to
life.txtRepeat until we get bored
We can easily script this solution in an infinite loop using Python.
The splendor and misery of this idea is that if you watch life.txt in any text editor that supports live content updates while the script is running, you can actually see the Game of Life in motion! And in ASCII graphics, no less. I would even say, in C+±syntax graphics, because this content, despite everything, remains C++ source code. C++ code that mutates every frame along with the game. It’s like some kind of evolutionary programming.
Writing the Main Loop
Here, the article takes a sharp linguistic turn and transforms from an article about C++ into an article about Python.
And someone might suddenly get indignant, because I’m basically cheating right in front of you and, while ostentatiously avoiding runtime in C++, I’m instead running off to a runtime in Python. There’s some truth to that, but I’m still not actually running the compiled EXE, so technically, the conditions of the challenge are not violated.
So, let’s sketch out our game loop:
def update(): compile_cpp() life = parse() render(life) def main(): while True: update() time.sleep(0.25) if __name__ == "__main__": main()
It’s all quite simple here—in an infinite loop with some delay, we’ll be hammering update(), which implements the sequence we described above: compilation, parsing, and “rendering.”
All that’s left for us is to implement the compile_cpp, parse, and render methods.
Compiling C++
The first thing we need to learn is how to run cl.exe from a Python script. Remember, it’s not that simple; we need to set up the environment first. So, we first run the batch file, then the compiler:
def compile_cpp(): ENVIRONMENT_SETUP = r'"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat"' COMPILE = 'cl /std:c++latest /O2 main.cpp' os.system(f'{ENVIRONMENT_SETUP} && {COMPILE}')
Note that I had to chain the two commands into one with && to push them through a single os.system call. The problem here is that each os.system call starts a new shell, meaning the context and environment are not preserved between os.system calls. So, if I had split the code into two commands, I would have gotten a complaint along the lines of “I don’t know who this cl is. and what you set up in the previous os.system stays in the previous os.system.”
Parsing the Binary
Now, after a successful compilation, let’s try to parse the EXE file and verify that all the desired data is indeed being parsed and extracted.
N = 16 with open('main.exe', 'rb') as f: f.seek(0xF320) for r in range(N): for c in range(N): print(int.from_bytes(f.read(1)), end=' ') print()
We get:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Everything is fine, the binary is being parsed, the magic offset still works. So we can finalize it:
def parse(): life = [] with open('main.exe', 'rb') as f: f.seek(0xF320) for r in range(N): life.append([]) for c in range(N): cell = int.from_bytes(f.read(1)) life[r].append(cell) return life
Rendering to life.txt
The main thing here is not to mess up the C++ syntax, otherwise we won’t get the next frame due to a compilation error in the C++ code on the next update() iteration:
def render(life): with open('life.txt', 'w') as f: for r in range(N): f.write('{') for c in range(N): cell = 'X' if life[r][c] == 1 else '_' f.write(f'{cell},') f.write('},\n')
Admiring the Result

Isn’t it a miracle:
constexpr-onlysource code mutation
source code as visual content
without running the binary
The Fight for FPS
You could say we’ve achieved what we wanted—we have a Game of Life simulation being calculated by a C++ compiler. But for complete satisfaction, I want to fix a few more problems with our solution.
The first thing that immediately catches the eye is the low FPS. It feels like something around 1 frame per second. On the one hand, what else did I expect—for every frame of the game, the compiler is launched from scratch and has to go through its entire standard cycle of parsing, optimization, and code generation. Remember I had time.sleep(0.25) in my main loop? I clearly got ahead of myself with that and have long since thrown it out—the animation above is already without that delay. And yet, the FPS figure is still extremely low.
We recall that our script is forced to run a .bat file every frame to set up the shell environment. By my empirical estimates, this step should take up at least half of the total working time, so we need to solve this problem and allow the script to run the batch file once at the start and somehow remember the acquired context. Hmm, another wish in the style of:

So, using os.system doesn’t let us remember the shell environment settings. I’m not a Python expert, but the documentation at docs.python.org states that if you need more capabilities than os.system, you can use subprocess.run.
Showing a lack of character and some laziness, I went to ChatGPT, and it came up with a solution for me using subprocess.run, possibly a hacky one, but it works nonetheless.
The key idea: via subprocess.run, we’ll call our batch file + the set command:
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" && set
What does the set command do? If called without arguments, it simply prints a list of all current shell environment variables to stdout. Something like this:
$ set HISTFILE='/root/.ash_history' HOME='/root' HOSTNAME='localhost' IFS=' ' LINENO='' OLDPWD='/' OPTIND='1' PAGER='less' PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' PPID='1' PS1='\h:\w\$ ' PS2='> ' PS4='+ ' PWD='/root' SHLVL='3' TERM='linux' TZ='UTC-01:00' _='--version' script='/etc/profile.d/*.sh'
What the cunning ChatGPT suggests:
Run the batch file
Run
setParse the stdout and save the parsed variables into a dictionary
We will also call the C++ compiler in a loop via
subprocess.run, which happens to have an optional advanced argumentenv=, and our parsed map fits perfectly into it!
I don’t even know how stupid or brilliant this is. Just like everything else in this article.
So, we write a setup() function that we’ll call at the start of the script:
msvc_env = {} def setup(): CMD = r'"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" && set' result = subprocess.run(CMD, stdout=subprocess.PIPE, shell=True) for line in result.stdout.decode().splitlines(): if '=' in line: key, value = line.split('=', 1) msvc_env[key] = value
And in compile_cpp(), we’ll replace os.system with subprocess.run and pass our environment variables to it:
def compile_cpp(): COMPILE_CMD = 'cl /std:c++latest /O2 main.cpp' result = subprocess.run( COMPILE_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=msvc_env )
Enjoying the result:

By my estimation, this is about 5 FPS. Compared to the old result of 1 FPS. So our efforts were not in vain, and the result has sped up not just by a factor of two, but by about five.
It’s also worth adding that the compiler has an /MP{n} flag, which makes it work in multiple threads, but my experiments showed that in my case, the payoff is vanishingly small.
Compile-time Randomness
Formally, we have a working version of the Game of Life, but it’s static, with a lone figure on the canvas. What I want is an explosive riot of cells interacting with each other. Filling the starting frame by hand is a tedious, thankless task; I’d like to randomize the first frame so that each run has an unexpected scenario for us.
These desires, in turn, create two more problems for us that we’ll have to solve somehow. First, how to get randomness in constexpr conditions? Second, we need to somehow explain to the compiler that it should fill the canvas randomly only on the first compilation. Subsequent runs should work simulatively, as is done in the current implementation.
We’ll postpone the last problem for now and focus on implementing randomness at compile-time.
The first thing that came to my mind was shaders. More precisely, how they sometimes implement simple pseudo-random sequences. As a rule, it’s not some overly complex random number generator with a fair distribution, etc., but some spaghetti code of mathematical operations slapped together almost at random. For example, I dug through my notes and fished this out:
float random (vec2 st) { return fract(sin(dot(st.xy, vec2(12.9898,78.233)))*u_time*1.0 ); }
Looks questionable? I probably can’t resist finishing the shader right before your eyes, for proof that it works quite well:
void main() { vec2 st = gl_FragCoord.xy/u_resolution.xy; st *= 10.0; st = floor(st); vec3 back = vec3(0.1, 0.9, 0.5); vec3 front = vec3(0.2, 0.2, 0.2); gl_FragColor = vec4(mix(back, front, random(st)), 1.0); }
And now look at the result:

Does it matter that the randomness here is very pseudo? Not at all. Moreover, you don’t even have to go to such lengths as in this shader—a primitive but decent pseudo-random number generator can be made with a simple formula containing operations that are very much available in a consteval context:
Or, in simple terms, for the next number in the sequence, we:
Multiply it by something large and, preferably, prime
Add something large and, preferably, prime
Take the modulo with something large and, preferably, prime, or even better—something cleverly bit-shifted
I settled on:
seq = (110351545 * seq + 12345) % (1 << 31);
The only trouble is that the sequence will always be the same if we don’t find a way to start it with some random seed. Yes, yes—to implement randomness, we need randomness, a classic. By the way, in the shader, the current time played the role of the seed.
I went searching the internet and at some point stumbled upon this GitHub snippet. You don’t have to follow the link; I’ll tell you right away the main thing I got from it—the person uses the __TIME__ macro as a seed. Yes, time again, but in a strange form.
Generally, this macro comes paired with the __DATE__ macro. Both of them work in a rather curious way. Let’s recall how macros work in principle: even before parsing, the compiler first runs the preprocessor, which performs text substitutions where the macros are located. Only after that does the actual compilation of the source code begin. Instead of __DATE__ and __TIME__, the preprocessor will substitute string literals of the current build time into the source code, like "Jan 14 2012" and "22:29:12". It’s actually interesting to realize that with such macros in your code, your source code reaches the compiler with slightly different content each time, since __TIME__ will expand into a different string constant every second. True, my experiments showed that the __TIME__ macro’s substitution updates for some reason only every 10 seconds, but this might be specific to the MSVC preprocessor implementation. For our needs, this limitation is not a hindrance.
In practice, the string gets embedded perfectly even in the asm file. If I write something like this in the C++ code:
constexpr const char* dd = __DATE__; constexpr const char* tt = __TIME__;
then a piece like this will appear in the asm listing:
; COMDAT ??_C@_08BCGBJOAF@20?313?330@ CONST SEGMENT ??_C@_08BCGBJOAF@20?313?330@ DB '20:13:30', 00H ; `string' CONST ENDS ; COMDAT ??_C@_0M@DINKGNNG@Nov?520?52024@ CONST SEGMENT ??_C@_0M@DINKGNNG@Nov?520?52024@ DB 'Nov 20 2024', 00H ; `string' CONST ENDS
And so I’ve revealed the time I was writing this article.
Okay—how do we use a const char* as a seed? By hashing it into a number, of course. We’ll write a homegrown hash as well. Here’s a function that returns a consteval seed right away:
consteval uint32_t seed() { uint32_t hash = 0; for (int i : {0, 1, 3, 4, 6}) { hash += static_cast<uint32_t>(__TIME__[i]); } return hash; }
Why the indices 0, 1, 3, 4, 6? If __TIME__ only changes every 10 seconds, then out of the constant "20:13:30", only these indices will actually change. And the last character at index 7 will always be the same, even though it’s not a :.
So, we combine the knowledge we’ve gained and give birth to the function consteval Canvas random_canvas():
consteval Canvas random_canvas() { Canvas res; uint32_t seq = seed(); for (uint32_t r = 0; r < N; ++r) { for (uint32_t c = 0; c < N; ++c) { seq = (110351545 * seq + 12345) % (1 << 31); res[r][c] = seq % 3 == 0; } } return res; }
What is seq % 3 == 0? Well, it’s a kind of threshold, a strange attempt to decide if a cell is alive or dead. I’m not sure how sound this solution is, but it ended up giving me good results in generating the first frame. For example, here:
{X,X,_,_,X,X,_,_,X,X,_,_,_,_,_,_,}, {_,X,_,_,_,X,X,X,_,_,_,_,_,X,X,_,}, {_,_,_,_,_,_,X,_,X,_,X,_,_,X,X,_,}, {_,X,_,_,_,_,_,_,_,_,_,_,_,_,X,_,}, {_,_,_,X,_,X,_,_,X,_,_,X,X,_,X,X,}, {_,_,_,_,_,_,_,_,_,X,X,_,_,_,_,_,}, {_,_,X,X,_,X,_,X,_,_,_,_,X,X,_,_,}, {_,X,X,_,_,_,_,_,_,_,_,_,X,_,_,_,}, {X,_,X,_,_,X,_,_,X,_,_,X,_,_,_,_,}, {X,_,_,_,X,_,X,_,_,X,X,X,_,X,X,_,}, {_,_,_,_,X,X,_,X,_,X,_,_,_,_,X,X,}, {_,X,_,_,_,X,X,_,X,X,_,X,X,_,_,_,}, {_,_,_,_,X,_,_,X,X,_,_,_,X,_,X,_,}, {_,_,X,_,_,_,_,X,_,_,X,_,_,_,X,_,}, {_,_,_,_,_,_,_,_,X,X,_,_,X,_,_,_,}, {_,X,_,X,_,_,X,_,X,X,X,_,_,X,_,_,},
I consider this a worthy first frame.
Implementing Conditional Compilation
The last obstacle in our path is to let the compiler know what to do: generate the first random frame or simulate a new frame based on the previous one?
This puzzle occupied me for a while, as it was initially difficult to wrap my head around how C++ code could know that it was being run for the first time and not the second. And then I remembered that this is C++, and here you can smear macros all over everything until you drop, and sometimes these macros can work wonders, as we’ve already seen with __TIME__.
It all turned out to be simple, I would even say prosaic—you can pass custom defines to any compiler when you run it, and then the code can behave differently based on them. For MSVC, this can be done with the /D{define_name} flag. And this story is as old as time; you just forget it at the most crucial moment because it’s fashionable these days to scold and shame people for using macros.
Anyway, the classics come into play. Before:
constexpr Canvas newLife = update(life);
After:
#ifdef STARTUP constexpr Canvas newLife = random_canvas(); #else constexpr Canvas newLife = update(life); #endif
The changes on the script side concern how the compiler is launched:
first_build = True def compile_cpp(): global first_build STARTUP_CMD = '/DSTARTUP' if first_build else '' COMPILE_CMD = f'cl /std:c++latest /O2 {STARTUP_CMD} main.cpp' result = subprocess.run( COMPILE_CMD, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, env=msvc_env ) first_build = False
On the first iteration of the main loop, the script will run the compiler like this:
cl /std:c++latest /O2 /DSTARTUP main.cpp
The rest of the runs will be without /DSTARTUP:
cl /std:c++latest /O2 main.cpp
The Finale

The frames are jerky due to a poor screen recording, but I couldn’t pass up leaving in the scenario with the giant spaceship at the end.
As a bonus, here’s me staring right into the soul of the EXE file while it’s being mercilessly changed by the compiler. I’m looking, but not running!

Far3 doesn’t provide such a good FPS, but the ability to see these guts is satisfying in itself. The Hex Editor extension for VS Code shows changes to the binary file faster, almost in real time, but it doesn’t have such cheerful smiley faces for the byte 01.
What Was That?
I’m asking myself the same question. However, let’s look back retrospectively at what we did and what we mastered:
Learned some subtleties of
constexprLearned to run MSVC without an IDE
Dug into the guts of an EXE with our bare hands and even found what we were looking for
Used macros, hehe
Started with a prayer, ended with Python
Learned the intricacies of
os.systemandsubprocess.runI showed you a shader for some reason
Managed to do compile-time randomness
Wrote a “game”
Didn’t run the binary, just stood next to it!
Had fun, after all
Abnormal programming makes life more fun. The main thing is, don’t bring this stuff into production.