SimplifyC++ Article

C or C++ for Systems Programming

By Ayman AlherakiReads: 148Today: 0

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:

while implementing the function internally using modern C++.

The public interface can remain extremely simple:

while the implementation uses:

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:

is much easier to expose to Python, Rust, Go, Java, C#, Swift, and many other languages than an interface containing:

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:

may couple the consumer to specific compiler and ABI details.

By contrast:

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:

then 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:

with:

You can move within the same program from something as low-level as:

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:

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:

For 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:

everywhere, 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:

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:

For 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.

Actual visitors 71,696
Visitors today 1,797
Total page views 1,709,395
Page views today 2,190
Book downloads 13,644