Home › Programming languages › Systems languages
guide low

C and C++

Problem Is C or C++ the right language for a project? This guide weighs maximum performance and the huge native ecosystem against the memory-safety and undefined-behaviour risks you must manage.

Maximum control and the largest native ecosystem (engines, drivers, HPC, finance), at the price of undefined behavior and memory-safety bugs you must actively prevent.

ccppsystemsundefined-behaviorraiicmakesanitizers
Overview

C is a thin layer over the machine and the ABI every language calls into; C++ adds zero-overhead abstractions (RAII, templates, containers). Both produce the fastest code and run everywhere, but the compiler assumes you never trigger undefined behavior: an out-of-bounds write or signed overflow can silently corrupt memory. Modern C++, sanitizers and hardening flags remove most of that risk, never all.

Key facts

Use when

  • Game engines, real-time audio/video, HFT, HPC, GPU host code, vendor C SDKs, existing C/C++ systems.
  • Real uses by field: games (Unreal Engine is programmed in C++), databases (PostgreSQL is about 1.78 million lines of C), browsers (Chromium), embedded devices (SQLite is a C library built for phones, set-top boxes, cameras and cars).

Avoid when

  • New network-facing services or parsers where Rust or Go fit.

Deep dive

Decision table

JobPickWhy
Library with a stable ABI for other languagesC (or C++ with extern "C" interface)Every FFI speaks the C ABI
Microcontroller with vendor HALC (C++ subset if supported)Vendor toolchains and examples
Game engine / real-timeC++20Unreal, console SDKs, tooling, talent
Numerics / HPCC++ with Eigen, OpenMP, MPI, or CUDAMature, vectorizes well
New service in an existing C++ shopC++20/23 with strict guidelinesReuse code and skills
New security-sensitive componentRust, called via C ABIMemory safety by default
BuildCMake (≥ 3.20) + Ninja; Bazel for monoreposMost libraries ship CMake
Packagesvcpkg or ConanReproducible third-party builds

Memory and performance model

  • C: malloc/free, pointers, no ownership tracking. C++: RAII (destructors run at scope exit), std::unique_ptr (sole owner, zero overhead), std::shared_ptr (atomic refcount, use sparingly), value semantics and move semantics.
  • Control over layout: struct packing, alignment (alignas(64) against false sharing), custom allocators (arenas, slabs), SIMD intrinsics.
  • Abstractions are zero-cost when inlined: templates monomorphize like Rust generics; virtual calls cost an indirect branch and block inlining.
  • UB examples the optimizer exploits: signed integer overflow, out-of-bounds access, use-after-free, data races, strict-aliasing violations, null dereference, uninitialized reads.

Concurrency model

OS threads (std::thread, std::jthread with stop tokens), std::mutex, std::atomic with the C++11 memory model, std::async, parallel algorithms (std::execution::par), C++20 coroutines (language feature; needs a library such as Asio or libunifex/stdexec for scheduling). Data races are UB; ThreadSanitizer finds them.

Code

Modern C++20: RAII for C resources, std::span instead of pointer+length, std::jthread joining automatically.

// g++ -std=c++20 -O2 -Wall -Wextra -pthread checksum.cpp -o checksum
#include <algorithm>
#include <cstdint>
#include <cstdio>
#include <memory>
#include <numeric>
#include <span>
#include <thread>
#include <vector>

struct FileCloser {
    void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); }
};
using File = std::unique_ptr<std::FILE, FileCloser>;   // closed exactly once, even on exceptions

uint64_t fnv1a(std::span<const uint8_t> data) {         // pointer + length travel together
    uint64_t h = 1469598103934665603ull;
    for (uint8_t b : data) { h ^= b; h *= 1099511628211ull; }
    return h;
}

int main() {
    std::vector<uint8_t> buf(1 << 24);
    std::iota(buf.begin(), buf.end(), uint8_t{0});      // wraps mod 256: unsigned, well-defined

    const unsigned n = std::max(1u, std::thread::hardware_concurrency());
    std::vector<uint64_t> partial(n);
    {
        std::vector<std::jthread> workers;              // destructors join
        const size_t chunk = buf.size() / n;
        for (unsigned i = 0; i < n; ++i) {
            const size_t begin = i * chunk;
            const size_t len = (i + 1 == n) ? buf.size() - begin : chunk;
            workers.emplace_back([&partial, &buf, i, begin, len] {
                // each thread writes only its own slot: no data race
                partial[i] = fnv1a(std::span<const uint8_t>(buf).subspan(begin, len));
            });
        }
    }
    uint64_t combined = 0;
    for (uint64_t p : partial) combined = combined * 31 + p;

    File out(std::fopen("checksum.txt", "w"));
    if (!out) { std::perror("fopen"); return 1; }
    std::fprintf(out.get(), "%016llx\n", static_cast<unsigned long long>(combined));
}

CMake with a sanitizer switch:

cmake_minimum_required(VERSION 3.20)
project(checksum CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(checksum checksum.cpp)
target_compile_options(checksum PRIVATE -Wall -Wextra -Wpedantic -Wconversion)
option(SANITIZE "Build with ASan + UBSan" OFF)
if(SANITIZE)
  target_compile_options(checksum PRIVATE -fsanitize=address,undefined -fno-omit-frame-pointer)
  target_link_options(checksum PRIVATE -fsanitize=address,undefined)
endif()

Release hardening flags (GCC/Clang on Linux, per the OpenSSF guide):

-O2 -D_FORTIFY_SOURCE=3 -D_GLIBCXX_ASSERTIONS -fstack-protector-strong \
-fstack-clash-protection -fcf-protection=full -fPIE -pie \
-Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack

Efficiency

  • Cost: the cost model is the hardware's: no runtime, no GC, a malloc/free pair per heap object unless you pool. Safety is bought back with tools: AddressSanitizer typically slows a program 2x (Clang docs), ThreadSanitizer about 5x-15x with 5x-10x more memory (Clang docs). Bound: cache misses and memory bandwidth, then branch and call overhead.
  • Levers:
    • ThinLTO (-flto=thin) — cross-module inlining with scalable, incremental link times (Clang docs) — large codebases
    • profile-guided optimization (-fprofile-instr-generate, IR-based instrumentation for best results) — better inlining and layout from real profiles (Clang docs; gain order of 10 %) — stable workloads
    • arenas and slabs instead of per-object new/malloc — fewer allocator calls and cache misses (order of) — many short-lived objects
    • alignas(64) per-thread data — removes false sharing (order of) — contended counters
    • ccache and Ninja — rebuilds in seconds (order of) — edit-compile loop, CI
  • Naive → standard → state of the art:
    • -O0, malloc per object · slowest, easiest to debug
    • -O2, RAII, containers with reserve · standard release build
    • -O3/-O2 with ThinLTO, PGO, arenas and SIMD intrinsics on measured hot loops · the fastest code available; sanitizer builds only in CI

Build and deploy

  • CI runs tests under ASan+UBSan and TSan builds, plus fuzzing (libFuzzer, AFL++) on every parser.
  • Static analysis: clang-tidy, -Wall -Wextra -Werror on new code; Clang's -Wunsafe-buffer-usage to find raw pointer arithmetic.
  • Ship statically linked where licensing allows, or in a container with pinned shared libraries.
  • Speed up builds: Ninja, ccache, precompiled headers, C++20 modules where your toolchain supports them.

Pitfalls

  • Raw new/delete and owning raw pointers; use unique_ptr, containers and values.
  • operator[] without bounds checks on untrusted indices; use .at() or hardened standard library modes in debug and CI.
  • Iterator and reference invalidation: holding a reference into a std::vector across push_back.
  • Signed overflow assumptions ("it wraps"); it's UB, the optimizer deletes your overflow check.
  • strcpy, sprintf, gets and manual length arithmetic in C; use bounded functions and track lengths explicitly.
  • Header-only template libraries blowing up compile times; ODR violations across shared libraries.
  • std::shared_ptr everywhere: atomic refcount traffic and unclear ownership.
  • Undefined evaluation order and lifetime of temporaries (dangling std::string_view to a temporary string).

Implementations

namelangrepoinstallverdictlicencehealthas_ofnotes
LLVM / ClangC++llvm/llvm-projectbrew install llvm————clang, clang-tidy, sanitizers, lld
GCCC / C++gcc-mirror/gccbrew install gcc————GNU compiler collection (read-only mirror)
CMakeC++Kitware/CMakebrew install cmake————de facto build generator
NinjaC++ninja-build/ninjabrew install ninja————fast build executor
ccacheC++ccache/ccachebrew install ccache————compiler cache

Sources

  • ISO/IEC 9899:2024 (C23) and ISO/IEC 14882:2024 (C++23) standards.
  • Stroustrup, Sutter (eds.), C++ Core Guidelines. docs
  • OpenSSF, Compiler Options Hardening Guide for C and C++. docs
  • Miller, Trends, challenges, and strategic shifts in the software vulnerability mitigation landscape, BlueHat IL 2019 (Microsoft 70 % figure).
  • Chromium Project, Memory safety (about 70 % of high-severity bugs). docs
  • LLVM project, AddressSanitizer docs, ThreadSanitizer docs, ThinLTO docs.
  • Epic Games, Programming with C++ in Unreal Engine. docs · PostgreSQL, About. site · SQLite, Appropriate uses for SQLite. docs

Alternatives and related

Choosing a languageWhich programming language should you use for a given job, and when is a rewrite or a mix of languages worthwhile? This guide compares languages by speed, memory, hiring and build cost.RustIs Rust the right language when you need native speed without a garbage collector and without memory-safety bugs? This guide weighs that against its learning curve and compile times.ZigIs Zig a suitable C replacement for low-level systems code and cross-compiling, and what does its pre-1.0 status cost? This guide helps decide between Zig, C and Rust.CUDA and GPU languagesHow should you program a GPU, and when is it worth writing custom kernels rather than using existing libraries? This guide helps pick between libraries, Triton and CUDA C++ for numeric workloads.False sharingMultithreaded code that should scale linearly gets slower as threads are added even though the threads share no data. This guide explains this hidden cache-level contention and how to avoid it.Arena allocatorPrograms such as servers, compilers, query engines and games allocate many small objects that all die together at the end of a request, pass or frame, and want allocation and cleanup to be nearly free.Slab allocatorPrograms that constantly create and destroy many objects of the same size (connections, timers, inodes) pay for searching, headers and fragmentation in a general-purpose allocator.Memory orderingCompilers and CPUs reorder memory operations, so lock-free code, flags and custom locks can break in ways one thread never sees. This guide answers what ordering guarantee each atomic operation needs.