Blog › ICP guides

C++ developer on retainer: modern C++20, RAII ownership, move semantics, and CMake on monthly retainer

September 4, 2026 · ~22 min read

A fintech platform had a production crash that only reproduced under load. The crash dump pointed to a destructor in a NetworkSession class, but valgrind reported clean on unit tests and stress tests in the development environment. AddressSanitizer identified it in the first run: heap-use-after-free in ~NetworkSession(). The root cause was a raw owning pointer — Socket* m_socket — with no user-defined copy constructor. The compiler-generated copy constructor performed a shallow copy. When a std::vector<NetworkSession> reallocated, it copied each element, producing two instances with the same underlying pointer. Both ran delete m_socket in their destructors. One double-free, one crash, reproducible only when the vector grew under load and triggered the internal resize.

The fix was not large. Replace Socket* m_socket with std::unique_ptr<Socket> m_socket. Delete the copy constructor and copy assignment operator (= delete) to make the class explicitly move-only. Write move constructor and move assignment operator using std::exchange(m_socket, nullptr) to leave the moved-from object with a null pointer the destructor handles safely. Audit the eighteen call sites where NetworkSession was stored or passed by value. Switch the std::vector<NetworkSession> to std::vector<std::unique_ptr<NetworkSession>> at twelve sites. Add std::move(session) at six emplace_back call sites. Total diff: forty lines across eleven files. Total work: eleven hours across two sessions — two hours reproducing with ASAN, five hours on the RAII refactor and rule-of-five analysis, three hours auditing call sites, one hour re-running ASAN and valgrind to confirm zero violations.

The production crash rate for NetworkSession double-free went to zero. No new feature shipped. The visible artifact of eleven hours of C++ advisory work was forty lines of change. The invisible artifact was the elimination of an entire class of ownership bugs — not just in NetworkSession, but across the eight other classes that received the same RAII audit during the call-site review. A C++ developer on monthly retainer does this category of work continuously: finding ownership violations before ASAN does, migrating raw pointer patterns before they produce crashes, and establishing the memory model that keeps sanitizers quiet.

The RAII, move semantics, and ownership work that retainers fund

RAII migration is the category of C++ retainer work that generates the most hours with the least visible output. A codebase that grew from C++98 to C++11 without systematic modernization typically has three patterns: raw owning pointers held as class members without following the rule of five (user-defined destructor, copy constructor, copy assignment, move constructor, move assignment — if any one of the five is user-defined, the compiler-generated versions of the others are likely wrong); resource handles (file descriptors, POSIX mutexes, OpenGL texture IDs, database connection handles) managed with explicit close/free calls that are skipped on error paths because exceptions bypass cleanup code that assumes linear execution; and output parameters passed as raw pointers where the caller cannot tell from the function signature whether the callee takes ownership, borrows, or optionally writes.

A retainer engagement covering RAII migration begins with an ASAN baseline: configure a sanitizer build type in CMake (target_compile_options(mytarget PRIVATE -fsanitize=address,undefined) and target_link_options(mytarget PRIVATE -fsanitize=address,undefined)), run the test suite under ASAN, and collect the full list of violations. Violations cluster by class: the classes that appear most often in ASAN traces are the ones with the most hazardous ownership model. Each cluster receives its own RAII refactor: the owning raw pointer becomes a std::unique_ptr with a custom deleter if the resource requires a non-delete release (e.g., std::unique_ptr<FILE, decltype(&fclose)> for FILE handles, or a RAII handle type with an explicit release function for OpenGL textures). Classes that hold non-copyable resources become move-only by deleting copy operations. Classes that genuinely need shared ownership get std::shared_ptr with std::weak_ptr observers to break cycles. The ASAN baseline before and after the refactor is the client-facing proof that eleven hours of invisible work closed eleven vulnerability classes.

Move semantics optimization is a different retainer category: not safety, but performance. C++ move semantics allow large objects (std::vector, std::string, std::map, custom containers with heap-allocated internals) to transfer ownership of their heap allocation instead of copying it. A retainer engagement covering move semantics typically involves: auditing function signatures where large objects are passed by value unnecessarily (a void process(std::vector<Record> records) parameter copies the entire vector on every call when a const std::vector<Record>& or a sink parameter with std::move at the call site would avoid the copy); auditing return sites where named return value optimization (NRVO) applies so that return result; constructs the return value in-place in the caller's frame rather than copying (and verifying with -fno-elide-constructors that the constructor count matches expectations); and auditing move constructors in custom types to ensure they are noexcept — std::vector only uses move semantics during reallocation when the element type's move constructor is declared noexcept, so a missing noexcept qualifier causes std::vector<CustomType> to copy rather than move during growth, spending double the heap allocation budget on each reallocation.

Modern C++20 concepts, ranges, and coroutines

C++20 concepts replace SFINAE as the primary mechanism for constraining templates. A retainer engagement covering C++20 concepts adoption typically begins with the SFINAE patterns in the existing codebase: template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>> T clamp(T value, T lo, T hi) produces a 40-line substitution failure error when called with a floating-point argument. The concepts replacement is template <std::integral T> T clamp(T value, T lo, T hi) — the error message becomes “constraint not satisfied: T=double does not satisfy std::integral.” The migration involves reading each SFINAE expression, translating it to a named concept or concept combination with && conjunction, and updating the requires clause. Hours are in understanding which SFINAE patterns are restricting the type to a single category (replace with a standard concept: std::integral, std::floating_point, std::totally_ordered, std::ranges::range) versus which are enforcing a structural requirement (write a custom concept with a requires expression: template <typename T> concept Serializable = requires(T t, std::ostream& os) { os << t; }).

C++20 ranges transform how algorithms compose. A retainer engagement covering ranges adoption converts algorithm chains that use intermediate vectors into lazy pipelines. The pattern std::vector<int> filtered; std::copy_if(input.begin(), input.end(), std::back_inserter(filtered), pred); std::transform(filtered.begin(), filtered.end(), output.begin(), transform); allocates an intermediate heap vector for filtered. The ranges equivalent auto result = input | std::views::filter(pred) | std::views::transform(transform); is lazy — no intermediate allocation, no iteration over the filtered range until result is consumed. The retainer work is identifying the allocation-heavy patterns, understanding which ranges views compose correctly (views::filter, views::transform, views::take, views::drop, views::zip in C++23), and handling the edge cases where lazy evaluation produces surprising behavior (views are not containers; they do not own their elements; a view over a temporary produces a dangling reference). Valgrind memcheck and sanitizers catch the dangling reference cases; the retainer developer catches the architectural cases where a view is stored in a struct past the lifetime of the range it views.

C++20 coroutines enable asynchronous programming with co_await, co_yield, and co_return, but the standard library does not ship a ready-made task or generator type — only the coroutine machinery (std::coroutine_handle, std::suspend_always, std::suspend_never). A retainer engagement covering C++20 coroutines typically involves selecting a library (cppcoro for task and generator; libcoro; or writing a minimal promise_type for the specific use case), integrating the coroutine task type with the event loop or I/O completion port, and auditing exception propagation through coroutine frames (an exception thrown inside a coroutine body that is not caught locally is stored in the coroutine frame and rethrown when the caller calls co_await or retrieves the result — missing this means exceptions silently disappear). The retainer hours are in understanding the coroutine transformation, not in the feature itself: C++20 coroutines are a language feature whose behavior is entirely determined by the promise_type the library author writes, and reading a new promise_type implementation correctly requires understanding the coroutine state machine that the compiler generates.

CMake build engineering, sanitizers, and profiling

CMake modernization is a third category of C++ retainer work that accumulates invisible hours. A CMakeLists.txt file that uses include_directories(${SOME_LIB_INCLUDE_DIR}) and add_compile_options(-Wall -Wextra) at the top level applies those settings to every target in the project, including test targets, example targets, and third-party subdirectories that were not intended to receive them. The modern CMake equivalent uses target properties: target_include_directories(mylib PUBLIC include/ PRIVATE src/) so that consumers of mylib receive only include/ in their include path (not src/), and target_compile_options(mylib PRIVATE -Wall -Wextra) so that the warning flags apply only to mylib and not to third-party code included via add_subdirectory. A CMake modernization retainer engagement reads the existing CMakeLists.txt files, maps the global settings to the targets that actually need them, converts each to target-scoped properties with correct visibility, and verifies that downstream packages that find_package(mylib) receive the correct include paths and compile definitions through the automatically generated mylibTargets.cmake export file.

FetchContent and vcpkg integration are the dependency management portion of the CMake modernization engagement. A codebase that vendors third-party libraries by copying their source into a third_party/ directory accumulates version drift: the vendored copy is whatever version was current when it was copied, and updating it requires manual replacement. FetchContent declares the dependency with a version tag (FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0)) and downloads it at configure time, making the version explicit in the CMakeLists.txt and reproducible across machines. vcpkg baseline pinning provides an alternative: a vcpkg.json manifest with a builtin-baseline commit hash ensures that every developer and CI run resolves the same library versions. The retainer work is evaluating which approach fits the project's offline build requirements, CI runner image constraints, and corporate firewall policies, and implementing the migration without breaking existing platform configurations.

Performance profiling with perf and Instruments produces the most underlogged retainer hours of any C++ engagement. A perf flame graph is a visual representation of where the CPU spent its time across the call stack, but reading one requires knowledge of the codebase's call graph, the compiler's inlining decisions, and the difference between on-CPU time (the function is executing) and off-CPU time (the function is blocked on I/O or a mutex). A retainer developer who has read flame graphs before identifies the hotpath — the tall, wide stack frame near the top of the flame graph — recognizes whether it is a genuine algorithmic hotspot or a symptom of cache misses (cache misses appear as cycles without forward progress, often detectable by comparing perf report output with perf stat -e cache-misses,cache-references), and applies the appropriate optimization (algorithmic restructuring for hotspots; data layout changes from array-of-structs to struct-of-arrays for cache miss patterns; std::move for copy hotspots; __builtin_expect for branch prediction hints on known-rare error paths). Logging that a performance session consumed seven hours but produced a three-line diff is the accurate description of what happened. Making that log entry legible to a non-C++ client requires naming the tool, the hotpath identified, and the before/after metric.

How HourTab tracks C++ developer retainer hours

C++ developer retainers present the sharpest possible version of the hour-visibility problem: a six-hour RAII migration session produces a diff of forty lines across eleven files, all of which are type signature changes. The session involved reading ASAN stack traces, tracing ownership chains through six layers of class hierarchy, deciding between unique_ptr and shared_ptr ownership models at three call sites, auditing whether each moved-from object was left in a valid state, and running the sanitizer suite three times to verify the fix. None of that reasoning appears in the diff. A time log entry that says “ownership refactor, 6h” accurately describes the session duration and leaves the client with no basis for understanding what the six hours bought.

HourTab gives C++ developers a public retainer-hours URL they paste into the first message of every client Slack thread. The client opens the URL and sees the current burn-down without asking. For C++ retainers specifically, the work log format matters more than the URL: each entry should name the translation unit or class, the diagnostic tool and its output (ASAN: heap-use-after-free in NetworkSession::~NetworkSession at session.cpp:47), the ownership decision made and the rationale (std::unique_ptr with move-only semantics rather than shared_ptr because only the session manager owns each session — shared ownership would complicate the shutdown sequencing), the call sites updated, and the before/after sanitizer metric (ASAN: 0 violations after refactor; valgrind memcheck: 0 errors). That entry is five structured fields that take four minutes to write and make the next client check-in a ten-second read rather than a twenty-minute explanation of what a heap-use-after-free is and why fixing it took six hours.

The retainer model fits C++ platform engineering because the work is continuous, not project-shaped. C++ standard evolution (C++17, C++20, C++23) introduces new features (ranges, concepts, coroutines, std::expected, std::mdspan) that require migration decisions and toolchain updates. Compiler updates surface new warnings (Clang 17 warns on -Wunsafe-buffer-usage for pointer arithmetic patterns) that require audit and remediation. Security audits require ASAN/UBSAN baselines. Performance requirements require profiling sessions. A project contract closes when the feature ships. A C++ retainer stays open for the next compiler update, the next security audit, and the next ASAN violation that only surfaces under production load.

Track C++ developer retainer hours without the status emails

HourTab gives systems engineers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.

See HourTab pricing →

FAQ: C++ developer retainers

What does a C++ developer on retainer typically do?

A C++ developer or systems engineer on monthly retainer provides ongoing RAII ownership design, std::unique_ptr/shared_ptr migration, C++20 concepts and ranges adoption, CMake target-scoped build engineering, AddressSanitizer/UBSan/TSan configuration and triage, move semantics optimization, template instantiation reduction, and performance profiling with perf or Instruments. The retainer covers the systems engineering between visible feature releases: ownership refactors, sanitizer baselines, SDK migration decisions, and profiling sessions that produce no new feature but eliminate a class of crashes or improve throughput.

What C++ work is most underlogged in a retainer?

RAII ownership migration (replacing raw owning pointers with std::unique_ptr and auditing call sites), sanitizer triage (running ASAN for the first time, classifying 20+ violations, fixing the genuine bugs), and CMake modernization (converting global include_directories and add_compile_options to target_include_directories and target_compile_options with PRIVATE/PUBLIC/INTERFACE visibility) are the three most systematically underlogged categories. Each produces a small diff and a large behavioral change that only surfaces in sanitizer output, crash rate metrics, or build dependency graph analysis rather than in a visible UI feature.

What are typical C++ developer retainer rates?

Entry-level C++ developers (1–3 years, C++11 basics, std::unique_ptr, Catch2) bill at $100–$175/hr. Mid-level C++ engineers (3–8 years, RAII design, C++20 concepts/ranges, ASAN/UBSAN, CMake target-scoped builds, perf profiling) bill at $160–$295/hr. Senior C++ architects (8+ years, template metaprogramming, lock-free data structures, coroutine promise_type design, ABI stability, cross-compilation toolchains, linker visibility) bill at $225–$430/hr. Firm rates run $190–$345/hr. Monthly retainer ranges: $5,000–$9,000/mo for advisory (15–30 hrs), $14,000–$28,000/mo for full-engagement (feature development plus ownership design plus profiling).

What should a C++ developer retainer agreement include?

A C++ developer retainer agreement should specify C++ standard version scope (C++14/17/20/23), platform and compiler scope (GCC/Clang/MSVC/cross-compilation), build system scope (CMake/Bazel/Meson), ABI stability scope (shared library versioning constraints), safety advisory scope (ASAN/UBSAN/TSAN baseline and triage), performance advisory scope (profiling sessions, memory bandwidth optimization, cache efficiency targets), and hour logging specifics (translation unit name, diagnostic tool output, before/after metric — crash rate, allocation count, frame time, compilation time).

How should C++ developer retainer hours be logged?

Log each C++ retainer session with: advisory category (RAII migration, move semantics audit, C++20 concepts authoring, ranges adoption, CMake modernization, ASAN/UBSAN triage, perf profiling, template instantiation reduction, ABI stability review, coroutine task design), specific class or translation unit, diagnostic tool and output (ASAN: heap-use-after-free in NetworkSession::~NetworkSession at session.cpp:47; perf report: 34% CPU in std::map::operator[] due to string copies), fix applied with rationale (std::unique_ptr move-only because session manager is sole owner; absl::flat_hash_map with string_view lookup to eliminate copy), and before/after metric (ASAN: 0 violations; perf: std::map drops from 34% to 2% of CPU time). Include compiler and sanitizer versions.