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_ptrfor exclusive heap ownership,std::shared_ptronly when shared lifetime is the actual model. - Views:
std::span<T>,std::span<const T>, andstd::string_viewfor 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 constexprfor 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/