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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- cpp-modern Installs to ~/.agents/skills/cpp-modern/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install cpp-modern The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project cpp-modern Installs to .agents/skills/cpp-modern/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/cpp-modern.zipunzip -o cpp-modern.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) cpp-modern PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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
- The problem is the CMake build itself: use cmake-builds
- The code is plain C hunting memory bugs: use c-memory-safety
- You are targeting microcontrollers with no heap: use embedded-c
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.
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
- Read
AGENTS.md,CMakeLists.txt(or Meson/Bazel files),CMakePresets.json,.clang-format,.clang-tidy. - 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. - Build once before editing so pre-existing warnings are not attributed to you.
Feature availability check
Library support lags the language. Check before using:
| Feature | Header / syntax | Notes |
|---|---|---|
Concepts, requires | C++20 | solid 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++23 | GCC 12+, libc++ 16+, MSVC 19.33+ |
std::print / println | <print> C++23 | GCC 14+, MSVC 19.37+, recent libc++; probe before relying on it |
std::ranges::to, views::zip, enumerate | C++23 | GCC 14 is the practical floor; check per view |
Deducing this | C++23 | GCC 14+, Clang 18+, MSVC 19.32+ |
Modules, import std; | C++20/23 | needs 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
| Situation | Use |
|---|---|
| Single owner | std::unique_ptr<T> via std::make_unique |
| Shared ownership with a real lifetime reason | std::shared_ptr via std::make_shared; break cycles with weak_ptr |
| Non-owning, non-null reference | T& |
| Non-owning, maybe null | T* (raw pointer means “does not own”) |
| Read-only string parameter | std::string_view |
| Contiguous buffer parameter | std::span<const T> / std::span<T> |
| Take ownership in a sink | pass by value and std::move into the member |
| Optional value | std::optional<T> |
| Operation that can fail with a reason | std::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::filtercachesbegin(); 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
autofor iterator and long template types; spell the type when it documents intent (std::size_t count = ...).enum classover plain enums;std::to_underlying(C++23) for conversions.constexprfunctions for compile-time-computable values;constevalonly 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). noexcepton move constructors and move assignment, orstd::vectorwill copy instead of move on reallocation.- Designated initializers for aggregates:
Options{.timeout = 5s, .retries = 3}. std::jthreadoverstd::thread(joins on destruction, has a stop token).
Lifetime traps to check in review
std::string_vieworstd::spanstored as a member, or returned, while pointing at a temporary or a local.auto& x = vec[0];thenvec.push_back(...): reference invalidated.- Lambdas capturing
[&]orthisthat 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::movefrom aconstobject 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>withmodernize-*,bugprone-*,cppcoreguidelines-owning-memory,performance-*as the project configures. -
clang-format --dry-run --Werroron 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.yaml
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.