SimplifyC++ Article
C or C++ for Systems Programming
C or C++ for Systems Programming?
If C++ Can Do Almost Everything, Why Do We Still Use C?
Whenever we talk about operating systems, compilers, runtimes, low-level tools, assemblers, linkers, drivers, and software close to the hardware, the traditional question returns:
C or C++?
But I think the more interesting question today is not:
Can C++ compete with C in systems programming?
It is:
If C++ can reach nearly the same low level, and can also expose C-compatible interfaces, why do we still split some projects between C and C++? Why not write everything in C++?
First: Does C++ Include All of C?
Almost, but not literally.
C++ evolved historically from C and retains a great deal of compatibility with it. Because of that, C++ can be written in a very low-level style that closely resembles C.
But modern C and modern C++ are separate languages with separate standards. Some valid C programs are not valid C++, and both languages have evolved in different directions.
So the old statement:
C++ = C + Classes
is technically too simplistic.
In practice, however, most of the capabilities that make C attractive for systems programming also exist in C++:
Pointers
Direct address manipulation
Bitwise operations
Unions
Simple-layout structures
Raw memory access
Inline assembly and compiler intrinsics
Memory-mapped I/O
Hardware register access
System calls
Custom allocation
No garbage collector
Freestanding environments
C++ does not prevent you from getting close to the hardware.
It simply gives you more tools above that level.
The Important Advantage: C ABI
This is where C++ has a very powerful card to play.
You can write:
extern "C" int forge_initialize(void);while implementing the function internally using modern C++.
The public interface can remain extremely simple:
typedef struct ForgeVM_Handle ForgeVM_Handle;
ForgeVM_Handle* forge_create(void);void forge_destroy(ForgeVM_Handle*);while the implementation uses:
class VirtualMachine{public: ...};
std::unique_ptr<InstructionDecoder> decoder;std::vector<Instruction> instructions;This distinction is extremely important.
The external ABI does not have to dictate the internal implementation language.
extern "C" exists precisely to make interoperability between C and C++ practical.
So Here Comes the Hard Question
If I can write:
C ABI outside + Modern C++ inside
why should I write entire parts of a project in C?
The answer is not:
Because C is faster.
Nor:
Because C is closer to the processor.
Nor:
Because C++ cannot operate at low level.
In most modern cases, those are not sufficient arguments.
The real reasons are different.
1. C Has Become the Universal ABI Language
One of C's greatest strengths today is not its syntax.
It is its historical position in the software ecosystem.
Operating systems, libraries, runtimes, programming languages, and FFI systems all understand C interfaces extremely well.
An API such as:
int compress( const void* input, size_t size, void* output);is much easier to expose to Python, Rust, Go, Java, C#, Swift, and many other languages than an interface containing:
std::vector<std::byte>std::stringstd::unique_ptr<T>The issue here is not the capability of C++.
It is binary interface stability and interoperability.
This is why you can write the core in C++ while keeping the public boundary in C.
2. C++ ABI Is More Complicated
Once you start exporting C++ classes across library boundaries, you enter a more sensitive world:
Name mangling
Object layout
Virtual tables
RTTI
Exceptions
Standard library ABI
Allocator ownership
Compiler versions
Compiler options
Exporting:
class Engine {public: virtual void run() = 0;};may couple the consumer to specific compiler and ABI details.
By contrast:
typedef struct Engine Engine;
void engine_run(Engine*);creates a much simpler boundary.
So:
The implementation may be C++, while the interface intentionally remains C.
That is not a contradiction.
It is often excellent architecture.
3. Some Environments Do Not Want a C++ Runtime
At the deepest layers of a system, there may not even be a runtime available yet.
You may be working in:
A bootloader
Kernel initialization
Bare metal
Firmware
Embedded systems
before you have:
A heap
Threads
Exception runtime
Standard I/O
Dynamic loading
This simplicity makes C extremely attractive.
But there is an important misconception here:
This does not mean C++ cannot work in such environments.
C++ can be used without exceptions, without RTTI, without dynamic allocation, and without large parts of the standard library.
In other words, disciplined freestanding C++ is completely possible.
4. Simplicity Itself Is an Engineering Advantage
Sometimes we simply do not need templates.
We do not need polymorphism.
We do not need exceptions.
We do not need a large library.
If a tiny low-level module only deals with:
registerinterruptmemory pagesyscalldevicethen C may be an excellent choice purely because of its simplicity.
Not because C is more powerful than C++, but because:
The problem itself does not require a larger language.
That is a perfectly valid argument for C.
5. History Is Heavier Than We Think
There is another major reason that is often ignored:
Existing code.
A project containing millions of lines of C, thousands of contributors, and decades of testing will not rewrite itself in C++ merely because modern C++ might offer a cleaner design.
Linux is an obvious example.
This does not prove that C++ is incapable of writing a kernel.
It proves something else:
The language decision in a huge project is not just a benchmark between two languages. It is history, ecosystem, tooling, contributors, policy, and decades of accumulated code.
Now Look at the Other Side: LLVM
LLVM gives us almost the opposite example.
LLVM and much of its surrounding infrastructure are written in C++.
And we are talking about:
Compiler infrastructure
Optimizers
Code generators
Linkers
Debugging infrastructure
Machine-level representations
These are deeply low-level domains.
So the claim:
"Compilers and low-level tools should be written in C"
does not really survive contact with modern reality.
Is C++ Slower Because It Has More Features?
This is another common misunderstanding.
The existence of a language feature does not mean you always pay for it.
If you do not use virtual dispatch, you do not pay for virtual dispatch.
If you do not use exceptions, your architecture does not have to depend on them.
If you do not use dynamic allocation, the language does not force it upon you.
One of the core historical principles behind C++ is:
Don't pay for what you don't use.
Templates and constexpr can even move entire computations from runtime to compile time.
So abstraction in C++ can often have little or no runtime cost.
This Is Why C++ Is So Attractive for Systems Programming
Because it can combine:
PointersRegistersRaw MemoryMachine Instructionswith:
RAIITemplatesConceptsconstexprStrong TypesSmart PointersGeneric AlgorithmsYou can move within the same program from something as low-level as:
*reinterpret_cast<volatile uint32_t*>(address) = value;to a strongly typed abstraction representing a processor, instruction, register, or executable section.
That is extremely powerful.
So Why Do Projects Mix C and C++?
Because different parts of the system serve different purposes.
For example:
Boot / ABI / tiny runtime ↓ C
Compiler / optimizer / linker ↓ C++
Public API ↓ C ABI
Application / tooling ↓ Modern C++This is not a weakness of either language.
It is an architectural decision.
But Do New Projects Still Need This Split?
This is where my position becomes more ambitious.
If I were starting a new low-level project today, without a huge historical C codebase forcing my hand, my first question would be:
What part can I genuinely not write in C++?
In many projects, the answer may be:
Almost nothing.
At that point, the entire system can be implemented in modern C++, while exposing C-compatible interfaces only where a stable public ABI is required.
In other words:
External World │ C ABI │ ┌──────────▼──────────┐ │ Modern C++ │ │ │ │ Compiler │ │ Assembler │ │ Linker │ │ JIT │ │ Runtime │ │ Tools │ └─────────────────────┘ │ Hardware / OSFor a new project, I find this more convincing than automatically splitting the codebase between C and C++ simply because older projects did so.
Do Not Use C++ Merely as C
There is another mistake to avoid.
If you choose C++ and then write:
malloc()free()raw pointergoto cleanupmanual resource managementeverywhere, then you are not gaining much from choosing C++.
The power of C++ is not merely that it allows you to write C-like code.
Its real power is that it allows you to descend to C-like low-level programming when necessary, then return immediately to safer abstractions when that low-level control is no longer needed.
For example:
FileHandle file;MemoryMapping mapping;Instruction instruction;Register<X86::RAX> rax;These abstractions can often be implemented with little or no runtime overhead while giving us much stronger resource management and type safety.
That is one of C++'s greatest strengths in systems programming.
So Should C Disappear?
Absolutely not.
C will remain extremely important.
It has:
Exceptional simplicity
A massive ecosystem
Compilers for almost everything
A universally important ABI
Huge historical libraries
Excellent support for tiny environments
Therefore, saying:
"C is obsolete because C++ exists"
would not be sound engineering.
But the Opposite Is Also Not True
Saying:
"We use C because systems programming requires C"
is no longer a sufficient argument either.
Modern C++ has proven itself in some of the most demanding areas of compiler infrastructure, machine code generation, runtimes, and low-level tooling.
The correct question is therefore not:
C or C++?
It is:
Where do I need the simplicity and ABI advantages of C, and where can C++ give me stronger abstractions without sacrificing hardware control?
Conclusion
There is a major difference between saying:
Use C
and saying:
Use a C ABI.
You may need the second without needing the first.
That distinction is often lost in debates about C versus C++.
A project can be written almost entirely in modern C++ while still exposing a clean and stable C interface to the outside world.
That gives us the best of both worlds:
C ABIfor interoperability and stability
+
Modern C++for implementation, abstraction,safety, and scalability
+
Assembly / Machine Codewhere absolute hardware control is requiredFor new projects involving compilers, assemblers, linkers, JITs, runtimes, and low-level programming tools, I believe this model deserves to be the starting point, rather than automatically assuming that the lowest layers must be written in C.
Modern C++ does not move us away from the machine.
It allows us to reach the machine when we need to, without forcing us to stay there all the time.
What did you think?
Sign in to react or comment.
Comments
0No comments yet.