conceptModern C++~6 min readUpdated 2026-07-02#cpp#c#raii#ownership#zero-cost

C vs C++: what you actually gain (and pay)

C gives you a thin contract with the machine: explicit storage, explicit lifetimes, simple translation units, and very few language-level abstractions. C++ keeps that low-level reach but adds ways to express ownership, invariants, generic algorithms, and overload-selected interfaces directly in code. The win is not "automatic safety"; C++ still has undefined behavior, raw pointers, layout concerns, and ABI reality. The win is that a disciplined subset lets the compiler enforce more of the resource protocol you were hand-maintaining in C.

The reset: C++ is not a magic safer C. It is a bigger language where the good subset can encode lifetimes and intent, and the bad subset can hide bugs behind more syntax.

How it really works

The useful C-to-C++ shift is not classes by themselves. It is lifetime as a language mechanism. Constructors establish an object invariant. Destructors run when that object's lifetime ends. Copy and move operations define what ownership transfer means. Templates let one implementation specialize at compile time without a void* protocol. The standard library then builds containers, strings, algorithms, smart pointers, locks, and filesystem handles on top of those rules.

C habit Modern C++ replacement What changes
malloc / free pairs std::vector, std::string, std::unique_ptr release is tied to object lifetime
goto cleanup on every exit RAII destructors cleanup is generated at normal and exceptional exits
void* plus callbacks templates, overloads, function objects type checking happens before runtime
out-parameters for ownership return values and move-only types ownership transfer is visible in the type
manual length bookkeeping containers and spans size travels with the data
macro-based generic code templates and constexpr code is checked after substitution, not pasted as text

C++ still compiles to object files, symbols, relocations, and machine code. A std::vector is not a runtime service; it is a small object containing pointers/capacity metadata plus library code that allocates, moves, and destroys elements. A template is not dynamic dispatch; it usually instantiates concrete code for the used types. std::unique_ptr<T> is normally just a raw pointer plus a deleter type, with copy disabled and move enabled.

The cost is that C++ has more rules at every layer. The language has overload resolution, temporary lifetime extension, special member functions, exception cleanup paths, template instantiation, one-definition-rule constraints, name mangling, and standard-library contracts. Those rules are not academic. They shape compile times, diagnostics, binary interfaces, generated code, and what you can safely expose across a C ABI boundary.

What you gain

  • Deterministic cleanup. RAII turns "remember to call cleanup on every path" into a destructor call that the compiler inserts at scope exit.
  • Stronger ownership vocabulary. A raw pointer can mean borrow, optional borrow, array, out-parameter, owned object, or C API handle. std::unique_ptr, std::shared_ptr, and references say more at the type boundary.
  • Regular value types. A type can own heap memory and still behave like a value if it defines copy, move, and destruction correctly.
  • Library density. std::vector, std::string, std::array, algorithms, iterators, and smart pointers remove a lot of C boilerplate without changing the machine target.
  • Compile-time abstraction. Templates, constexpr, concepts, and overloads can move checks from runtime protocols into compile-time selection.
  • Exception-aware cleanup. Even if your project avoids exceptions at boundaries, C++ library code is designed around destructors running during stack unwinding.

What you pay

  • Language complexity. You need a subset. "All of C++" is not a style; it is a hazard.
  • Compile-time cost. Templates and headers can make the build graph heavier than the C equivalent.
  • ABI complexity. Name mangling, exceptions, RTTI, vtables, standard-library versions, and allocator choices matter when shipping binary interfaces.
  • Hidden code paths. Constructors, destructors, conversions, allocation, and overloaded operators can run where a C reader would see only a declaration or expression.
  • Still no memory-safety guarantee. Dangling references, invalid iterators, data races, unchecked indexing, strict-aliasing bugs, and lifetime mistakes still exist.
  • Freestanding friction. Kernels and bootloaders can use C++ subsets, but hosted assumptions such as exceptions, RTTI, global constructors, and parts of the standard library need deliberate toolchain support.

Executable artifact: the same cleanup problem in C and C++

The runnable demo lives in examples/modern-cpp/c-vs-cpp-what-you-actually-gain/. It builds a C version that uses manual cleanup and a C++ version that lets objects clean themselves when a normal return or an exception leaves the scope.

cd examples/modern-cpp/c-vs-cpp-what-you-actually-gain
./run.sh

The C program has to centralize ownership by convention:

IntBuffer buffer = {0, NULL};
char *label = NULL;

label = malloc(strlen(name) + strlen(" report") + 1);
if (label == NULL) {
    goto cleanup;
}

if (!int_buffer_init(&buffer, 4)) {
    goto cleanup;
}

cleanup:
    int_buffer_destroy(&buffer);
    free(label);

The C++ program makes ownership a property of objects:

class IntBuffer {
public:
    explicit IntBuffer(std::size_t count) : items_(count) {}
    int sum() const;

private:
    std::vector<int> items_;
};

static int make_report_cpp(const std::string &name, bool fail_after_alloc) {
    IntBuffer buffer{4};
    std::string label = name + " report";

    if (fail_after_alloc) {
        throw std::runtime_error("simulated error");
    }

    return buffer.sum();
}

The important thing is not that std::vector is fancy. It is that the release path is not a separate social contract. If control leaves the C++ scope after buffer and label have been constructed, their destructors run. To inspect the generated mechanism, compile the C++ side with assembly output:

g++ -std=c++20 -O2 -S demo.cpp -o demo.O2.s

In Compiler Explorer, compare the C cleanup labels with the C++ destructor paths. The C++ version has more language machinery, but after optimization the ordinary successful path is still direct code plus calls to the library operations you actually used.

Failure modes & trade-offs

  • Using C++ as "C with nicer syntax" loses the main benefit. Raw owning pointers, naked new/delete, and manual cleanup recreate C's problems with a larger language.
  • Using every C++ feature at once loses the plot. Multiple inheritance, implicit conversions, exceptions everywhere, deep templates, and global state can make code harder to audit than disciplined C.
  • RAII is not garbage collection. It is deterministic destruction. Cycles through std::shared_ptr, detached threads, global lifetime order, and leaked owning raw pointers still leak.
  • C ABI boundaries need plain contracts. C++ names, exceptions, destructors, and standard-library types should not leak across a C plugin or kernel ABI unless you control both sides tightly.
  • Zero-cost is conditional. You do not pay for unused features, but used features still have their real costs: allocation, reference counting, virtual dispatch, exception tables, code size, or cache behavior.

In practice

  • Learn C first, then use C++ to encode the discipline. This atlas is C-first because the OS project needs the raw machine model. C++ becomes useful when it removes repeated ownership protocol, not when it hides the machine.
  • Prefer values and RAII handles. std::vector<T>, std::string, std::unique_ptr<T>, and small invariant-holding classes should be the default before raw allocation.
  • Make ownership explicit at APIs. Use raw pointers and references for borrows, std::unique_ptr for transfer, and std::shared_ptr only when shared lifetime is the real model.
  • Keep generated code inspectable. Use -Wall -Wextra, sanitizers, Compiler Explorer, and object-file tools. C++ abstractions are acceptable when you can explain what they compile into.
  • Define a project subset. A kernel, an embedded runtime, a GUI app, and a server do not need the same C++ rules.

Connects to: RAII: the idea that changes everything · Smart pointers: unique_ptr, shared_ptr, ownership · Move semantics and value categories · Why C still matters · Undefined behavior: the contract you didn't know you signed · Memory leaks & ownership discipline · The pipeline

Sources

  • Bjarne Stroustrup - A Tour of C++ - concise map of the modern language and standard library for experienced programmers. https://www.stroustrup.com/tour3.html
  • C++ Core Guidelines - the practical "modern C++" rule set, especially resource management, interfaces, and the zero-overhead framing. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
  • cppreference - Classes - reference map for constructors, destructors, special members, and class mechanics. https://en.cppreference.com/w/cpp/language/classes
  • cppreference - Object model - object lifetime, alignment, storage, and type rules that keep C++ tied to machine reality. https://en.cppreference.com/w/cpp/language/object
  • cppreference - The rule of three/five/zero - the ownership-special-member rule behind value-like resource-owning types. https://en.cppreference.com/w/cpp/language/rule_of_three
  • Compiler Explorer - the daily tool for checking what C++ abstractions become under a real compiler. https://godbolt.org/