Modern C++ skill for Codex

C++20/23 with clear ownership and zero raw deletes. Codex checks the oldest compiler in your CI matrix before using a C++20 or C++23 feature, then applies a clear ownership table: unique_ptr for single owners, raw pointers only as non-owning, span and string_view for parameters, expected for recoverable errors. Reviews look for the lifetime traps that sanitizers catch late, and every change is built with high warning levels, ASan and UBSan.

  • Languages
  • $cpp-modern
  • 158 lines
  • 2 files
  • ~1,700 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- cpp-modern

Installs to ~/.agents/skills/cpp-modern/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $cpp-modern, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes and refactors C++ to C++20/23 standards - RAII and the rule of zero, unique_ptr and shared_ptr ownership, span and string_view parameters, ranges and views, concepts instead of SFINAE, std::expected and std::format/print, constexpr - and proves it with warnings, sanitizers and clang-tidy. Use when modernising legacy C++ with raw new/delete, reviewing ownership and lifetimes, adopting ranges or concepts, or deciding which C++23 features the project's compilers support.

Use it when

  • Modernising legacy C++ that uses owning raw pointers or manual cleanup
  • Reviewing C++ code for ownership, lifetime and dangling references
  • Adopting ranges, concepts, format or expected in an existing codebase
  • Choosing which C++23 library features are safe for the toolchain

Use something else when

How to check the result

The build is warning-free at high warning levels on every CI compiler, tests pass under ASan and UBSan, and clang-tidy reports no new findings on changed files.

The mistake it prevents

Storing a std::string_view or std::span member that points at a temporary, which compiles cleanly and dangles at runtime.

~/.agents/skills/cpp-modern/SKILL.md

--- name: cpp-modern description: Writes and refactors C++ to C++20/23 standards - RAII and the rule of zero, unique_ptr and shared_ptr ownership, span and string_view parameters,… license: MIT ---

Modern C++ code has no owning raw pointers, no manual delete, no unchecked lifetime assumptions hidden behind string_view or span, and compiles cleanly with high warning levels on every compiler the project supports. New features are used only when every target toolchain has them.

Before changing code

  1. Read AGENTS.md, CMakeLists.txt (or Meson/Bazel files), CMakePresets.json, .clang-format, .clang-tidy.
  2. Find the language standard (CMAKE_CXX_STANDARD, target_compile_features, -std=) and the compiler matrix in CI (GCC, Clang, MSVC versions). The oldest compiler decides which library features you may use.
  3. Build once before editing so pre-existing warnings are not attributed to you.

Feature availability check

Library support lags the language. Check before using:

FeatureHeader / syntaxNotes
Concepts, requiresC++20solid in GCC 10+, Clang 10+, MSVC 19.28+
Ranges, views<ranges>usable in GCC 10+, MSVC; Clang/libc++ caught up around 15-16
std::format<format>GCC 13+, Clang/libc++ 17+, MSVC 19.29+; else use fmtlib
std::expected<expected> C++23GCC 12+, libc++ 16+, MSVC 19.33+
std::print / println<print> C++23GCC 14+, MSVC 19.37+, recent libc++; probe before relying on it
std::ranges::to, views::zip, enumerateC++23GCC 14 is the practical floor; check per view
Deducing thisC++23GCC 14+, Clang 18+, MSVC 19.32+
Modules, import std;C++20/23needs CMake 3.28+ and a recent toolchain; do not introduce casually

Verify with a feature-test macro when in doubt (#if __cpp_lib_expected >= 202202L) or compile a one-line probe with each CI compiler. Keep a fallback (fmtlib, tl::expected) when the matrix includes older toolchains.

Ownership rules

SituationUse
Single ownerstd::unique_ptr<T> via std::make_unique
Shared ownership with a real lifetime reasonstd::shared_ptr via std::make_shared; break cycles with weak_ptr
Non-owning, non-null referenceT&
Non-owning, maybe nullT* (raw pointer means “does not own”)
Read-only string parameterstd::string_view
Contiguous buffer parameterstd::span<const T> / std::span<T>
Take ownership in a sinkpass by value and std::move into the member
Optional valuestd::optional<T>
Operation that can fail with a reasonstd::expected<T, E> (or the project’s error type)

Rule of zero: a class that only holds RAII members declares no destructor, copy or move operations. If you must write one of the five, write or = delete all five deliberately.

class Connection {
public:
    static std::expected<Connection, std::error_code> open(std::string_view host);
    std::expected<std::size_t, std::error_code> send(std::span<const std::byte> data);

private:
    explicit Connection(unique_fd fd) : fd_(std::move(fd)) {}
    unique_fd fd_;   // RAII wrapper; Connection gets correct move and no copy for free
};

Wrap C handles with a unique_ptr and a custom deleter when no wrapper exists:

struct FileCloser { void operator()(std::FILE* f) const noexcept { if (f) std::fclose(f); } };
using unique_file = std::unique_ptr<std::FILE, FileCloser>;

unique_file f{std::fopen(path.c_str(), "rb")};
if (!f) return std::unexpected(std::error_code(errno, std::generic_category()));

Ranges and algorithms

auto active_names(std::span<const User> users) -> std::vector<std::string> {
    auto names = users
        | std::views::filter([](const User& u) { return u.active; })
        | std::views::transform(&User::name);
    return {names.begin(), names.end()};          // C++23: std::ranges::to<std::vector>()
}
  • Prefer std::ranges::sort(v, {}, &User::age) (projection) over custom comparators.
  • Views are lazy and usually non-owning: never return a view over a local container.
  • views::filter caches begin(); iterating a filter view, then mutating the underlying elements so the predicate changes, is undefined behaviour territory. Materialise when in doubt.

Concepts instead of SFINAE

template <typename T>
concept Hashable = requires(const T& t) {
    { std::hash<T>{}(t) } -> std::convertible_to<std::size_t>;
};

template <Hashable Key, typename Value>
class Cache { /* ... */ };

Replace std::enable_if_t overload sets with constrained templates or if constexpr; error messages improve and intent is visible.

Other defaults

  • auto for iterator and long template types; spell the type when it documents intent (std::size_t count = ...).
  • enum class over plain enums; std::to_underlying (C++23) for conversions.
  • constexpr functions for compile-time-computable values; consteval only when a runtime call must be an error.
  • Mark single-argument constructors explicit; mark functions [[nodiscard]] when ignoring the result is a bug (error codes, expected).
  • noexcept on move constructors and move assignment, or std::vector will copy instead of move on reallocation.
  • Designated initializers for aggregates: Options{.timeout = 5s, .retries = 3}.
  • std::jthread over std::thread (joins on destruction, has a stop token).

Lifetime traps to check in review

  • std::string_view or std::span stored as a member, or returned, while pointing at a temporary or a local.
  • auto& x = vec[0]; then vec.push_back(...): reference invalidated.
  • Lambdas capturing [&] or this that outlive the scope (callbacks, threads, async).
  • Range-for over a temporary’s member: for (auto& x : get_obj().items()) dangles before C++23 lifetime extension fixes; bind the object first.
  • std::move from a const object silently copies.
  • Moved-from objects: only assign to or destroy them.

Verification

cmake --preset dev && cmake --build --preset dev
ctest --preset dev --output-on-failure
  • Compile with -Wall -Wextra -Wpedantic -Wconversion -Wshadow (GCC/Clang) or /W4 /permissive- (MSVC); new code adds no warnings.
  • Run tests under AddressSanitizer and UndefinedBehaviorSanitizer (-fsanitize=address,undefined -fno-omit-frame-pointer -g); separately ThreadSanitizer for concurrent code.
  • clang-tidy -p build <files> with modernize-*, bugprone-*, cppcoreguidelines-owning-memory, performance-* as the project configures.
  • clang-format --dry-run --Werror on changed files.
  • Build on the oldest compiler in the CI matrix before claiming a feature is safe.

When modernising legacy code, change one pattern at a time (all owning raw pointers in a module, then all output parameters) with tests between, and report what was converted and what was left for a follow-up.

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Modern C++"
  short_description: "C++20/23 with clear ownership and zero raw deletes"
  default_prompt: "$cpp-modern Replace the raw new and delete in this C++ module with smart pointers"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc