Generic coding rules and preferences distilled from existing agent rule files, project AGENTS.md/.rules, and saved memories: - strictness maximal; suppressions must be reported to the user - never filter build/lint/test output through grep/tail/head - reuse > fork > self-implement; generic code becomes its own package - 1000-line file / 100-line function limits, split by concern - regression test every fixed bug; never test against real user data - events over polling; scope discipline; honest reporting Per-language: rust, js-typescript, bash, python, go, csharp-dotnet, c-cpp. Each extends coding-dev.md rather than repeating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
6.5 KiB
Markdown
154 lines
6.5 KiB
Markdown
---
|
|
name: c-cpp
|
|
description: >-
|
|
Use this skill when writing, building, or debugging C or C++ — including compiler warning
|
|
configuration, sanitizers, modern C++ resource management, build systems (CMake/Meson),
|
|
32-bit/cross builds, and FFI boundaries exposed to other languages.
|
|
---
|
|
|
|
# C / C++ — Rules & Preferences
|
|
|
|
Extends [`../coding-dev.md`](../coding-dev.md). Everything there applies; this adds C/C++ specifics.
|
|
|
|
C and C++ are for the cases with no better option: existing codebases, hard real-time paths,
|
|
and FFI boundaries. **For new standalone work, prefer Rust** — the memory-safety guarantees
|
|
are worth more than the familiarity.
|
|
|
|
---
|
|
|
|
## 1. Warnings — Everything On, Errors
|
|
|
|
```cmake
|
|
target_compile_options(mytarget PRIVATE
|
|
-Wall -Wextra -Wpedantic -Werror
|
|
-Wshadow -Wconversion -Wsign-conversion
|
|
-Wnon-virtual-dtor -Wold-style-cast -Wcast-align
|
|
-Wunused -Woverloaded-virtual -Wdouble-promotion
|
|
-Wformat=2 -Wnull-dereference -Wimplicit-fallthrough
|
|
)
|
|
```
|
|
|
|
- **`-Werror` in the build gate.** `-Wconversion` and `-Wshadow` are noisy on first contact and
|
|
catch real bugs — do not drop them because a legacy file complains; fix the file or scope the
|
|
suppression to it.
|
|
- Run **both** GCC and Clang where possible; each diagnoses things the other misses.
|
|
- `clang-tidy` with `bugprone-*`, `cert-*`, `performance-*`, `modernize-*`.
|
|
- `clang-format` with a committed `.clang-format`. No hand-formatting.
|
|
|
|
---
|
|
|
|
## 2. Sanitizers — Use Them
|
|
|
|
```bash
|
|
cmake -DCMAKE_BUILD_TYPE=Debug \
|
|
-DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"
|
|
```
|
|
|
|
- **ASan + UBSan on every debug/test build.** They find the bugs that warnings cannot.
|
|
- TSan for anything threaded (separate build — it is incompatible with ASan).
|
|
- Valgrind when a sanitizer build is not possible.
|
|
- **A sanitizer finding is a bug, not noise.** Fix it.
|
|
|
|
---
|
|
|
|
## 3. Resource Management (C++)
|
|
|
|
- **RAII for everything.** Every raw `new`/`delete`, `malloc`/`free`, `fopen`/`fclose`, and
|
|
native handle pair is a leak waiting for an early return or a throw.
|
|
- `std::unique_ptr` by default; `std::shared_ptr` only where ownership is genuinely shared.
|
|
`std::weak_ptr` to break cycles.
|
|
- **No owning raw pointers.** A raw pointer or reference in a signature means "borrowed,
|
|
non-owning, outlives the call".
|
|
- `std::string_view` / `std::span` for non-owning views — and never return one that outlives
|
|
its backing storage.
|
|
- **The Rule of Zero**: design so no destructor, copy, or move needs writing. If you must write
|
|
one, write all five.
|
|
- `constexpr`, `const`, and `noexcept` wherever they hold. `[[nodiscard]]` on anything whose
|
|
return value must not be dropped.
|
|
|
|
---
|
|
|
|
## 4. C Specifics
|
|
|
|
- C17 or later unless the target forbids it.
|
|
- **Check every allocation and every return value.** In C, unchecked returns are the primary
|
|
bug source.
|
|
- Fixed-width types from `<stdint.h>` (`uint32_t`), never bare `int` for anything with a
|
|
defined width on the wire.
|
|
- Always the bounded string functions (`snprintf`, `strncpy` with explicit termination); never
|
|
`strcpy`, `sprintf`, `gets`.
|
|
- `static` on every function and global not exported from the translation unit.
|
|
- Prefer a struct-and-functions module boundary with an opaque pointer over exposing layout.
|
|
|
|
---
|
|
|
|
## 5. Build Systems
|
|
|
|
- **CMake** (3.20+, target-based) or **Meson** for new projects. Never a hand-written Makefile
|
|
for anything non-trivial.
|
|
- Target-based CMake only: `target_link_libraries`, `target_include_directories`,
|
|
`target_compile_options`. **No directory-level `include_directories` or global
|
|
`CMAKE_CXX_FLAGS` mutation.**
|
|
- `PRIVATE` / `PUBLIC` / `INTERFACE` chosen deliberately — this is what keeps include leaks out
|
|
of dependents.
|
|
- **Register every new source file with the build system** in the same commit that adds it. A
|
|
source file the build never compiles will sit there looking correct while every edit to it
|
|
does nothing.
|
|
- Vendor dependencies with `FetchContent` or submodules; document which and why.
|
|
|
|
---
|
|
|
|
## 6. Cross & Multi-Arch Builds
|
|
|
|
- Keep 32-bit and 64-bit outputs in **separate build directories**; never reuse one.
|
|
- **A component built for one architecture cannot load one built for another.** An
|
|
`ELFCLASS64` / `ELFCLASS32` error means a loader path is reaching the wrong build — check the
|
|
library search path, not the code.
|
|
- When a build must match an already-installed runtime's version, **pin it explicitly** and
|
|
note the constraint. A silently mismatched pair fails at load time with a message that
|
|
points nowhere useful.
|
|
- Record the exact toolchain and container used for a cross build. "It built on my machine" is
|
|
not reproducible for native code.
|
|
|
|
---
|
|
|
|
## 7. FFI Boundaries
|
|
|
|
When exposing C/C++ to another language:
|
|
|
|
- `extern "C"` on the boundary, with a hand-written, stable, documented header.
|
|
- **Ownership across the boundary must be explicit** — who allocates, who frees, and with which
|
|
function. Provide a matching `free` for every allocation you hand out.
|
|
- **Never let an exception cross an `extern "C"` boundary.** Catch everything at the edge and
|
|
convert to an error code.
|
|
- Bindings generated by a tool (`bindgen`, etc.) are regenerated, never hand-edited.
|
|
- On the consuming side, keep all `unsafe`/unmanaged interop in **one thin wrapper module**
|
|
that exposes a safe API — never scattered through call sites.
|
|
|
|
> [!WARNING]
|
|
> **Binding crates/packages that reach into a library's struct fields break when that library
|
|
> makes the struct opaque.** Pin to a binding version that matches the installed library
|
|
> generation, and note the constraint where someone will read it before bumping.
|
|
|
|
---
|
|
|
|
## 8. Testing & Debugging
|
|
|
|
- GoogleTest or Catch2; CTest integration so `ctest` runs the suite.
|
|
- **Regression test every fixed bug.**
|
|
- Debug builds keep frame pointers (`-fno-omit-frame-pointer`) so stack traces are usable.
|
|
- `-g` even in release builds; ship separate debug symbols rather than none.
|
|
- **Record dead ends in the project's notes**, especially for native debugging — the
|
|
investigation that ruled something out is expensive and will otherwise be repeated. Note
|
|
what did *not* work alongside what did.
|
|
- When a binary search through a packed/embedded blob finds nothing, that is not evidence the
|
|
content is absent — packed asset formats defeat naive string searches. Re-check without
|
|
anchoring before concluding.
|
|
|
|
---
|
|
|
|
## 9. Related
|
|
- [`../coding-dev.md`](../coding-dev.md) — the general rules this extends.
|
|
- [`rust.md`](./rust.md) — the preferred language for new native work, and the usual consumer
|
|
of these FFI boundaries.
|