conceptModern C++~5 min readUpdated 2026-07-02#cpp#style#systems#guidelines#subset

Which subset of C++ to use (and which to avoid)

C++ is not a single language in practice. It is a toolbox large enough to write a kernel, a GUI toolkit, a template metaprogramming library, or an unreadable accident. Low-level C++ works best when the project chooses a house subset: the features that make ownership, layout, and invariants explicit, plus the features it bans or quarantines because they hide runtime cost.

The reset: "we use C++" is not a policy. "We use RAII, move-only ownership, spans for views, explicit errors at system boundaries, no raw owning new, and no RTTI/exceptions in the kernel half" is a policy.

How it really works

A good subset is not anti-modern. It is selective. It keeps features whose mechanics are visible and whose generated code can be inspected; it avoids features that create global lifetime surprises, ABI traps, accidental allocation, or non-local control flow where the runtime cannot support it.

Prefer Be careful with Usually avoid in low-level hot paths
RAII wrappers custom destructors with tricky ownership manual paired cleanup spread across returns
std::unique_ptr and move-only types std::shared_ptr across unclear ownership graphs raw owning pointers
std::span and std::string_view returning views views into temporaries
std::vector, std::array, std::string allocator behavior and relocation std::list by default
enum class and strong types too many tiny wrappers unscoped enum soup
std::optional, std::variant, expected weak or string-only error types sentinel values that overlap valid data
templates and concepts compile time and code size unconstrained template interfaces
lambdas in local algorithms capturing by reference across lifetime boundaries type-erased callbacks in hot loops
assertions and sanitizers recoverable errors as asserts continuing after violated invariants

Project policy sits above this table. Google C++ and LLVM both make no-exceptions choices for large-codebase and systems reasons. The C++ Core Guidelines recommends RAII, explicit ownership, and exceptions for some contract failures. Those are not contradictions; they are different runtime and compatibility constraints.

Executable artifact: a small house subset

The runnable demo lives in examples/modern-cpp/which-subset-of-cpp-to-use/. It compiles with -fno-exceptions -fno-rtti to show a systems-friendly subset: RAII for logging, enum class for protocol tags, std::span for a byte view, std::optional for local parse failure, and no raw owning allocation.

cd examples/modern-cpp/which-subset-of-cpp-to-use
./run.sh

The parser takes a view, not ownership:

static std::optional<Packet> parse_packet(std::span<std::uint8_t const> bytes)
{
    if (bytes.size() < 2)
        return std::nullopt;

    auto kind = decode_kind(bytes[0]);
    if (!kind.has_value())
        return std::nullopt;

    std::size_t payload_size = bytes[1];
    if (bytes.size() != payload_size + 2)
        return std::nullopt;

    return Packet {
        *kind,
        std::vector<std::uint8_t>(bytes.begin() + 2, bytes.end()),
    };
}

The ConnectionLog object is boring RAII: construction opens the scope, destruction closes it, copying is deleted. That is the shape you want for file descriptors, locks, temporary mappings, trace scopes, and any other paired operation.

The subset I would start with

  • Baseline: C++20 for new hosted systems projects, with C++23 features admitted only after compiler and standard-library checks.
  • Ownership: value types by default, std::unique_ptr for exclusive heap ownership, std::shared_ptr only when shared lifetime is the actual model.
  • Views: std::span<T>, std::span<const T>, and std::string_view for non-owning parameters, with lifetime documented by the caller/callee relationship.
  • Error policy: exceptions in exception-safe hosted code; expected-style values at system, parser, allocator, driver, and no-exceptions boundaries.
  • Generic code: concepts on public templates, if constexpr for type-dependent branches, and small headers that do not instantiate a universe.
  • Containers: std::array, std::vector, and contiguous layout first; node containers only when their specific invalidation or splice behavior matters.
  • C interop: extern "C" at ABI boundaries, fixed-width types where layout matters, and explicit adapters around errno or pointer-length APIs.
  • Forbidden by default: raw owning new/delete, C-style casts, owning raw pointers, global mutable state, unchecked narrowing, and clever overload sets that require a language-lawyer pause.
  • Quarantined by policy: exceptions, RTTI, coroutines, std::function, custom allocators, static initialization with side effects, and runtime polymorphism in hot paths.

Failure modes & trade-offs

  • A subset that is only oral tradition. If the rules are not written, every review re-litigates taste.
  • Banning too much. If the subset forbids RAII, vector, or templates entirely, people re-create weaker versions with macros and manual cleanup.
  • Allowing everything in cold code and discovering hot code later. A nice plugin API can become a per-packet call path. Re-check abstractions when code moves.
  • Confusing no-exceptions with no errors. Disabling exceptions requires a stronger explicit error discipline, not less design.
  • ABI drift. Templates, inline functions, and standard-library types are awkward as stable binary interfaces across compilers or library versions.
  • Static initialization surprises. Global objects with constructors can create order-of-initialization bugs and startup cost.

In practice

Write the subset as a short engineering document and enforce it with compiler flags, warnings, sanitizers, code review, and examples. The point is not purity. The point is that a reader can look at a function signature and know who owns memory, who may fail, who may allocate, who may throw, and what lifetime the references depend on.

Then keep a pressure valve. Some code genuinely needs runtime polymorphism, type erasure, shared ownership, or exceptions. The subset should make those choices visible and reviewable, not impossible.

Connects to: C vs C++: what you actually gain (and pay) · RAII: the idea that changes everything · Smart pointers: unique_ptr, shared_ptr, ownership · Zero-cost abstractions · Error handling: exceptions vs expected · SerenityOS: an operating system written in C++ · Memory leaks and ownership discipline · Dynamic linking and shared libraries

Sources

  • C++ Core Guidelines - broad reference for ownership, resource management, interfaces, and error-handling policy. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
  • Google C++ Style Guide - real large-codebase subset with explicit bans and trade-offs, including exceptions policy. https://google.github.io/styleguide/cppguide.html
  • LLVM Coding Standards - systems-project subset emphasizing portability, no RTTI/exceptions, and code-size discipline. https://llvm.org/docs/CodingStandards.html
  • cppreference - std::span - non-owning contiguous view details and invalidation caveats. https://en.cppreference.com/w/cpp/container/span
  • cppreference - std::optional - vocabulary type for maybe-a-value return paths. https://en.cppreference.com/w/cpp/utility/optional
  • cppreference - std::variant - closed-set alternative storage and visitation model. https://en.cppreference.com/w/cpp/utility/variant
  • cppreference - Constraints and concepts - public template requirements and overload constraints. https://en.cppreference.com/w/cpp/language/constraints
  • Compiler Explorer - check whether a policy choice changed calls, branches, layout, or code size. https://godbolt.org/