The Problem That Persists
C++ doesn't have a memory safety story. It has a "discipline and tooling" story. AddressSanitizer catches bugs at runtime. Static analyzers catch some at compile time. The C++ Core Guidelines are a style guide, not an enforcement mechanism. And after three decades of incremental fixes, CVEs rooted in use-after-free still ship regularly and buffer overflows.
The NSA's 2022 guidance, CISA's subsequent push, and now the 2025 EU Cyber Resilience Act requirements have made "just be more careful" an untenable position for anything touching regulated industries. Hence the need to evaluate actual alternatives.
Carbon: Google's Interop-First Bet
Carbon's pitch is straightforward: be to C++ what Kotlin was to Java. Full bidirectional interop, gradual migration, and a modernized syntax that eliminates the most dangerous footguns.
Interop in Practice
The following example imports a C++ audio buffer class in Carbon:
import Cpp library "audio/ring_buffer.h";
fn ProcessBlock(buf: Cpp.RingBuffer*, block_size: i32) -> bool {
var samples: auto = buf->ReadBlock(block_size);
if (samples.size() != block_size) {
return false;
}
// Carbon's bounds-checked access
var i: i32 = 0;
while (i < block_size) {
samples[i] = ApplyGain(samples[i], 0.7);
++i;
}
return buf->WriteBlock(samples);
}
The import Cpp directive is genuinely impressive when it works. Testing shows that an entire ring buffer implementation, including std::atomic fence operations, can be imported and called from Carbon without writing a single line of binding code. The compiler understands C++ name mangling, vtable layouts, and even std::unique_ptr semantics.
Where it falls apart: SIMD code. Codebases using GCC vector extensions and __attribute__((target("avx2"))) extensively will find that Carbon's interop layer chokes on these. Every SIMD function needs to be wrapped in a plain C-linkage wrapper, which defeats the purpose of seamless interop.
Migration Cost Assessment
For a representative 180k-line codebase, the estimated breakdown:
- Clean modern C++17 code (~60% of the codebase): ~2.5 engineer-months to port
- Template-heavy generic code (~25%): ~6 engineer-months, with significant redesign
- SIMD/intrinsics/platform-specific (~15%): Not feasible to port. Keep as C++ and interop.
Total realistic estimate: 8-10 engineer-months for an incomplete migration. That is the honest number rarely shared at conferences.
Cpp2/cppfront: Herb Sutter's Pragmatic Middle Ground
Cpp2 takes a fundamentally different approach. Instead of building a new language with interop, it defines a new syntax that transpiles to standard C++20. Your existing build system, your existing debugger, your existing profiler, they all just work because the output is C++.
The Syntax in Action
The same ring buffer processing logic in Cpp2:
ProcessBlock: (inout buf: RingBuffer, block_size: int) -> bool = {
samples := buf.ReadBlock(block_size);
if samples.size() != block_size {
return false;
}
for (0 ..< block_size) do (i) {
samples[i] = ApplyGain(samples[i], 0.7);
}
return buf.WriteBlock(samples);
}
Notice the inout parameter, Cpp2 makes mutation explicit at the call site. Parameters are in (const ref) by default, which eliminates an entire class of accidental-mutation bugs.
What Actually Transpiles
The cppfront compiler turns that into roughly this C++20:
auto ProcessBlock(RingBuffer& buf, int block_size) -> bool {
auto samples = buf.ReadBlock(block_size);
if (samples.size() != block_size) {
return false;
}
for (auto i = 0; i < block_size; ++i) {
samples[i] = ApplyGain(samples[i], 0.7);
}
return buf.WriteBlock(samples);
}
The key insight: cppfront adds initialization safety (no uninitialized variables), null safety (no null dereference on cpp2::unique_ptr), bounds safety (checked subscript by default), and type safety (no implicit narrowing conversions). All enforced at the Cpp2 syntax level, all compiled away to zero-overhead C++20.
The Catch
Cppfront's error messages are… not great. When something goes wrong during transpilation, you get an error pointing to a line in your Cpp2 source, but if the error is in the generated C++, your compiler barfs on generated code you didn't write. A common frustration: spending an entire afternoon debugging a template instantiation failure that turns out to be a missing requires clause that cppfront should have generated but did not.
Also, IDE support is limited. There's a VS Code extension with basic syntax highlighting, but no reliable go-to-definition, no refactoring tools, and the language server crashes on files over ~500 lines. This matters more than people think.
Val: The Mutable Value Semantics Approach
Val (formerly known as Hylo) takes the most radical departure from C++. Its core premise: if every value is uniquely owned and mutations always happen in-place, you don't need a borrow checker, you don't need garbage collection, and you get memory safety with zero runtime overhead.
Ownership Without Borrowing
fun processBlock(buf: inout RingBuffer, blockSize: Int) -> Bool {
var samples = buf.readBlock(blockSize)
if samples.count != blockSize {
return false
}
for i in 0 ..< blockSize {
&samples[i] = applyGain(samples[i], 0.7)
}
return buf.writeBlock(samples)
}
The & prefix on samples[i] is Val's way of saying "I'm mutating this in-place." If you forget it, the compiler refuses to compile. It's explicit, it's visible, and it eliminates the "did I accidentally copy or alias this?" class of bugs.
What makes Val genuinely interesting compared to Rust is the absence of lifetime annotations. There are no 'a lifetimes sprinkled through your function signatures. Val's type system ensures safety through value semantics alone, if you have a value, you own it. Period. No shared mutable state, no data races by construction.
Current Limitations of Val
Val is the youngest of the bunch, and it shows. The compiler (built on Swift's SIL) is slow, compiling a 3000-line test module took 47 seconds versus 3 seconds in Cpp2 and 8 seconds in Carbon. The standard library is minimal. There's no FFI story worth mentioning, you can call C functions through a @foreign annotation, but C++ interop doesn't exist yet.
For a greenfield project with full stack control? Val's design is arguably the most elegant. For migrating an existing C++ codebase? It's a non-starter in 2026.
Circle: Sean Baxter's One-Man Revolution
Circle is the wildcard. Sean Baxter has single-handedly built a C++ compiler that extends the language with first-class safety features, pattern matching, reflection, and what amounts to a new language that happens to be a strict superset of C++.
Safety Annotations in Practice
#feature on safety
void ProcessBlock(RingBuffer& buf, int block_size) safe {
auto samples = buf.ReadBlock(block_size); // returns std2::vector
if (samples.size() != block_size) {
return;
}
for (int i = 0; i < block_size; ++i) {
samples[i] = ApplyGain(samples[i], 0.7); // bounds-checked
}
buf.WriteBlock(std2::move(samples));
}
The safe keyword on the function signature enables Circle's borrow checker for that function. You can mix safe and unsafe code at the function level, which is the most granular escape hatch of any language here.
Circle compiles existing C++ codebases, even 180k-line projects, on the first try. It is a C++ compiler, so naturally it does. The migration path is: compile the code with Circle, then incrementally add #feature on safety to files one by one, fixing the safety violations the compiler reports.
The Bus Factor Problem
Circle is maintained by one person. One incredibly talented person, but one person. There's no formal specification, no committee, no corporate backing. If Sean decides to pursue other interests, the project is effectively dead. For a hobby project, that's fine. For production infrastructure that needs to be maintained for a decade? That risk is real and it's the thing that limits broader recommendations of Circle.
Benchmark: Lock-Free Queue Implementation
The same Michael-Scott lock-free queue was implemented in all four languages plus baseline C++20. Here are the results on an AWS c7g.2xlarge (Graviton3, 8 vCPUs):
| Language | Enqueue/Dequeue ops/sec | Binary Size | Compile Time |
|---|---|---|---|
| C++20 (baseline) | 14.2M | 48 KB | 1.8s |
| Carbon | 13.8M | 52 KB | 4.2s |
| Cpp2 (transpiled) | 14.1M | 48 KB | 2.1s |
| Val | 11.6M | 67 KB | 12.4s |
| Circle | 14.2M | 49 KB | 2.3s |
Cpp2 and Circle are essentially identical to baseline C++ in performance, which makes sense, they emit C++. Carbon is within 3%, which is impressive for a different compiler backend. Val's 18% penalty comes from its more conservative alias analysis; the compiler couldn't prove that certain pointer operations in the lock-free queue were non-aliasing.
Practical Recommendations
For organizations with an existing large C++ codebase and regulatory pressure to improve memory safety:
- Circle for the fastest path to safety annotations on existing code, for those who can accept the single-maintainer risk.
- Cpp2 for the safest organizational bet, the output is standard C++, so you can always "eject" by just keeping the transpiled code.
- Carbon for those willing to wait 12-18 more months for the tooling to mature and the codebase is relatively clean modern C++.
- Val only for greenfield projects where you want the most elegant safety model and don't need C++ interop.
None of these are Rust. Rust has a decade head start in ecosystem, tooling, and production battle-testing. But for shops where 200 engineers know C++ and retraining to Rust would cost two years of velocity, these successors represent the pragmatic middle path.
The migration from C++ will not be a weekend project. It will not even be a quarter-long project for any non-trivial codebase. But the regulatory pressure is not going away, and starting now, even with imperfect tools, beats waiting for a perfect solution that may never arrive.