Skip to content

Back to Blog

8 min read ·

Nobody chooses C++ for fun

Nobody chooses C++. The machine, the latency budget or the SDK chooses it for you, and the only decision left is how carefully you write it.

I did choose it once, for fun. I wrote an assembly language interpreter that draws the CPU's wiring and its memory in the terminal, because I wanted to watch what a computer does between one instruction and the next. That's the fun version. In the version that pays salaries, somebody made the call years before you showed up.

The forces that choose

A trading engine that competes on microseconds can't afford a pause it didn't schedule. A garbage collector, the part of a runtime that frees memory you stopped using, decides on its own when to clean up, and even the modern ones stop your program for a moment to do it. If that moment lands in the middle of an order, the trade belongs to somebody else.

A game studio can build a lot in Unreal Engine with its visual scripting. A studio that wants to change how the engine itself behaves writes C++, because C++ is the language Unreal hands its programmers.

An Arduino Uno Rev3 has 2 KB of RAM. Two kilobytes. The sketch you type into the Arduino IDE looks like a friendly little language of its own, but the build tool turns it into a C++ program before it compiles a single line.

And then there's the vendor. Say you build controllers for somebody's hardware, and the vendor ships its SDK, the only way into that hardware, as C++ headers and a compiled library you can't read. No Python package, no REST API, no Rust crate. Nobody held a language debate. A zip file arrived.

The case against is real

On February 26, 2024, the White House's Office of the National Cyber Director published a report called "Back to the Building Blocks". It tells technology manufacturers to adopt memory-safe languages, and it names C and C++ as widespread languages that lack memory safety.

Memory safety means the language won't let you touch memory you don't own: past the end of an array, or after the object that lived there was freed. The report cites industry analysis showing that in some cases, "despite rigorous code reviews," up to 70 percent of the patched vulnerabilities in memory-unsafe languages that got a CVE (the public ID a security hole gets once it's disclosed) are memory-safety bugs. Its hall of fame runs from the Morris Worm in 1988 through Slammer and Heartbleed to the BLASTPASS exploit chain in 2023.

Read "despite rigorous code reviews" again.

That isn't a skill issue a stern senior engineer can review away. Careful people with careful reviewers wrote those bugs, for thirty-five years.

Then turn one page.

The report stops to consider space systems and lists what they need from a language. Code that sits close to the kernel and the hardware. Determinism, so the timing of the outputs stays consistent. And no garbage collector, or one you can override. Then it names who passes:

At this time, the most widely used languages that meet all three properties are C and C++, which are not memory safe programming languages.

Rust has all three properties, the report adds, "but has not yet been proven in space systems."

Now take the word "space" out of that list. Close to the hardware, predictable timing, no collector you can't control. That's the requirements document for a trading engine, a game loop and a microcontroller with 2 KB of RAM. The report says as much itself, a few lines later, when it widens the point to "space or other embedded systems that face similar constraints."

It calls space systems "a unique edge case." Whole industries live in that edge case.

"Why not Rust?"

Show me the SDK.

Rust is a good language. If that vendor shipped a Rust crate, I'd try it the same day. They don't. So you write bindings, and your safe program becomes a thin layer of Rust wrapped around the same C++ library. Rust treats every call into foreign code as unsafe unless you personally promise it is safe, because, as the Rust book puts it, "other languages don't enforce Rust's rules and guarantees, and Rust can't check them." Either way, the promise is yours. The vendor has never seen it and won't support it.

Telling a company with a working C++ codebase to rewrite it in Rust is correct advice in the way that flossing every day is correct advice. Even the report doesn't ask for that. For existing code it suggests a hybrid: find the riskiest libraries and rewrite those first. Most of the C++ that will run in 2034 is already written.

So the real question was never whether to write C++. It's which C++.

C with regrets

Bjarne Stroustrup called the first version of his language C with Classes. A lot of what the report complains about is a descendant I call C with regrets: raw new and delete, char buffers sized by hope, pointer arithmetic and C-style casts, written today as if 2011 never happened.

C++11 gave the language auto, lambdas, move semantics and smart pointers like std::unique_ptr, which frees its object when it goes out of scope so nobody has to remember to. There has been a new standard every three years since.

Modern C++ is nicer to read. It is still not memory safe.

std::vector<int> v{1, 2, 3};

// C++98
for (std::vector<int>::iterator it = v.begin(); it != v.end(); ++it)
    std::cout << *it << '\n';

// C++11
for (const auto& x : v)
    std::cout << x << '\n';

// Also C++11. Compiles clean. Undefined behavior.
for (const auto& x : v)
    if (x == 2) v.push_back(4);

The second loop can't be off by one. The third one compiles without a single warning, even with Clang's extra-warning flags -Wall -Wextra switched on, and it's a use-after-free.

Here's what happens. A std::vector keeps its elements in one block of memory with room for a fixed number of them, called its capacity. While there's room, push_back writes into the next free slot. When the vector is full, meaning size equals capacity, push_back can't grow in place. It allocates a bigger block, copies the elements across and frees the old one. cppreference is blunt about what that costs: after a reallocation, "all iterators (including the end() iterator) and all references to the elements are invalidated."

v{1, 2, 3} starts full, because every mainstream standard library gives it exactly three slots. When x reaches 2, the push reallocates, and x is now a reference into freed memory. So is the hidden iterator the loop uses to walk the vector.

123size 3 = capacity 3full, so it moves, then freedx still points herepush_back(4)1234size 4 · capacity 6
The vector is full, so push_back copies every value into a bigger buffer and frees the old one. Nothing tells x, which still points at a slot that no longer belongs to the vector.

Compile with AddressSanitizer (-fsanitize=address) and the program stops on the first run with heap-use-after-free, pointing at the loop.

Now the part that should scare you. Add v.reserve(8) before the loop, so there's spare room and nothing moves. AddressSanitizer goes quiet. The loop is still wrong. A range-for loop saves end() once, before it starts, and push_back invalidates the end iterator even when it doesn't reallocate. You traded a crash for a bug that waits. On my machine the loop quietly stops before the 4 it just added. That's undefined behavior, so another compiler is free to do something else, and nothing will warn you.

The pretty syntax didn't save that loop. Knowing what push_back costs would have.

Two questions per line

For as long as somebody else's decision keeps you in C++, make every line answer two questions: who owns this object, and what can invalidate this reference?

Then let the machine check your answers, the same way you'd make any confident system show its work. AddressSanitizer and UBSan, its sibling for undefined behavior like signed integer overflow, go in every test build. AddressSanitizer costs about a 2x slowdown, which in a test build is nothing. clang-tidy, which reads the source and flags the regret patterns before a reviewer sees them, runs on every pull request. The build turns all of it on by default, so nobody has to remember. And when you need plumbing like a thread pool, you take it from Boost or Qt, libraries more people have debugged than you will ever hire.

The tools are free, and there's a White House report on the desk. Skipping them is negligence, and it lands on users who will never read the code.

An SDK can choose your language. It cannot make you write C with regrets.