with one click
cpp-patterns
Use when implementing C++ code for Particle-Viewer, handling GL resources, working with SDL3, or applying DRY/deprecation/docs-commit patterns.
Menu
Use when implementing C++ code for Particle-Viewer, handling GL resources, working with SDL3, or applying DRY/deprecation/docs-commit patterns.
Use when adding new classes, refactoring code, or reviewing PRs for Particle-Viewer.
Use when a task needs design exploration before any implementation begins. Required for any task with unclear approach, significant architecture impact, or multiple valid solutions.
Use when building, adding dependencies, configuring CMake options, troubleshooting build failures, or managing Flatpak packaging for Particle-Viewer.
Use when writing or reviewing C++ code, running pre-commit checks, or addressing formatting, naming, or static analysis violations.
Use when writing tests for any interface, abstract base class, or type with multiple implementations.
Use when writing or reviewing any C++ class that owns resources, has a destructor, or acquires in a constructor.
| name | cpp-patterns |
| license | MIT |
| description | Use when implementing C++ code for Particle-Viewer, handling GL resources, working with SDL3, or applying DRY/deprecation/docs-commit patterns. |
NO C++ THAT VIOLATES RESOURCE MANAGEMENT OR BREAKS EXISTING ABSTRACTIONS
YOU MUST clean up all GL resources in destructors and update documentation in the same commit as any public interface change. No exceptions.
Violating the letter of this rule is violating the spirit of this rule.
Announce at start: "I am using the cpp-patterns skill to [implement/review] [description]."
.cpp, not the header.[+] All met -> proceed
[-] Any unmet -> load the code-quality skill, apply RAII, search for existing implementations, read the target code, or move the implementation detail into the .cpp before writing any production code
When writing C++ code for this project, apply these patterns consistently.
WindowConfig, SphereParams)QuadVertex with x, y, u, v) instead of raw float arrays"rb" mode for cross-platform correctnessA system that terminates on detecting bad state causes less damage than one that continues in an unknown state. ExceptionHiding -- silently swallowing an error -- transfers the symptom downstream where the root cause is invisible.
| Error type | Mechanism | Example |
|---|---|---|
| Programmer error (invariant violation) | assert() | assert(vao != 0 && "VAO must be initialized before bind") |
| Unrecoverable state | std::terminate() or throw | GL context creation failure |
| User-recoverable error | Log + return false/error code | Missing particle data file |
// WRONG -- ExceptionHiding: swallows compile error, propagates broken shader
glCompileShader(shader);
// CORRECT -- FailFast: surface at source
GLint success = 0;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success) {
char log[512];
glGetShaderInfoLog(shader, 512, nullptr, log);
std::cerr << "Shader compile error: " << log << "\n";
std::terminate();
}
Rule: Never check GL/SDL error returns and then continue silently. Log and terminate for unrecoverable state; log and return false for recoverable failures.
gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress)Viewer-Assets/shaders/glGetIntegerv(GL_VIEWPORT, ...) over cached viewport values where viewport may changegl_PointSize clamped by GL_POINT_SIZE_RANGE (max 256px on Mesa/llvmpipe)flatpak skillSDL_Init must include the flag for every SDL3 subsystem used. Missing a flag causes silent failure -- no error, no events, no devices.*ForID query functions: Use SDL_Get*ForID() variants (e.g., SDL_GetJoystickGUIDForID) over opening a joystick just to read a value then close it. Unnecessary open consumes a file handle.SDL_GetJoystickTypeForID() returns SDL_JOYSTICK_TYPE_GAMEPAD on SteamOS but SDL_JOYSTICK_TYPE_UNKNOWN on generic Linux (OpenSuse, Ubuntu) because stock udev rules don't set the GAMEPAD property. Always pair type-based detection with a capability-based fallback: open the joystick, check SDL_GetNumJoystickAxes() >= 4 && SDL_GetNumJoystickButtons() >= 6, then close it.Camera::KeyReader(). KeyReader has a single-press dispatch block -- calling it every frame fires single-press handlers repeatedly. For hold-buttons that mirror keyboard held keys, add a dedicated method (e.g., Camera::setSpeedBoost(bool)).inline to avoid multiple-definition linker errorsSee docs/IMGUI_INTEGRATION.md for architecture, FetchContent setup, menu system, and overlay positioning.
When removing a call site from viewer_app.cpp, do not also delete the supporting Camera public method unless it is architecturally wrong. The Camera API is stable; call sites in viewer_app change frequently with user preferences. Only remove a Camera method if it is architecturally wrong, not merely unused at the current moment.
DRY means every piece of knowledge has a single authoritative representation. Not text.
Acid test: If you change one copy and not the other, does the system break? If yes: DRY violation. If no: independent representations of different things -- not a violation.
frame_count * element_size to get bytes), and the derivation is the same everywhere, add the derived quantity as a virtual method on the interface. The concrete class is the single source of truth; callers never re-derive it.When you encounter a code smell or quality violation while working on something else:
[BROKEN WINDOW NOTED: src/viewer_app.cpp:142 -- raw glDrawArrays outside IOpenGLContext]
Place this in a // TODO at the point of observation. After your current task completes, create a follow-up todo to address it. One window = one todo.
Deprecating a symbol is NOT complete until every call site is removed or explicitly annotated.
Before marking anything deprecated:
grep -r "FunctionName" src tests// TODO: migrate to [replacement]A [[deprecated]] attribute with active, unannotated call sites is annotated debt, not deprecation.
When you change a public interface (function signature, class API, behavior visible to callers), update documentation in the same commit.
Applies to:
docs/ markdown files describing the changed interfaceDocumentation out of sync with the interface actively misleads. Not a follow-up commit. Same commit.
These smells are not caught by clang-tidy. Catch them in code review.
| Smell | OpenGL Manifestation | Fix |
|---|---|---|
| MagicNumber | Raw GLenum inline: glTexImage2D(..., 0x8051, ...) | Named constant: constexpr GLenum kInternalFormat = GL_RGB8 |
| DataClumps | (GLuint vao, GLuint vbo, GLuint ebo) always passed together | Extract struct GpuMesh { GLuint vao, vbo, ebo; } |
| PrimitiveObsession | int textureUnit for GL_TEXTURE0 binding point | enum class TextureUnit : GLint or typed wrapper |
| ArrowAntiPattern | if (gladLoad()) { if (SDL_Init()) { if (createWindow()) { ... } } } | RAII wrappers -- each resource cleans itself up on scope exit |
| SpeculativeGenerality | Abstract render interface with exactly one concrete implementation | Remove the abstraction until a second implementation exists |
| CopyAndPasteProgramming | VAO setup duplicated per render pass; shader variants copied with minor edits | setupVAO() extracted function; OpenGL Shading Language (GLSL) #define or UBO for variants |
| ExceptionHiding | glCompileShader() with no glGetShaderiv(GL_COMPILE_STATUS) check | Always check, log, and terminate on unrecoverable GL errors (see FailFast above) |
SDL_Init called without the subsystem flag for a subsystem being used#include| Excuse | Reality |
|---|---|
| "I'll clean up the GL resources in a follow-up" | GL leaks cause test failures and memory issues; clean up now |
| "The deprecation is signaled with [[deprecated]], that's enough" | Call sites still run deprecated code. Remove them or annotate them. |
| "Docs can be updated separately" | Stale docs mislead the next developer. Same commit is the rule. |
| "It's a transitive include, it works" | Transitive includes break when source headers change. Be explicit. |
| "Removing the call site is enough to remove the feature" | If the Camera method is architecturally valid, keep it. Only the call site was user preference. |
| "The caller ensures this field is valid before I use it." | No guard + no test = a wish. If there is no static_assert, no bounds check, and no test enforcing the invariant, it is unspecified behavior. Add the guard. |
cpp-safety -- sub-domain skill; destructor must not throw and every resource must have a scope-bound owning guard -- load this skill when the class owns any resourcecode-quality -- clang-format and naming conventions; cpp-patterns covers structural patterns, code-quality covers formoop-principles -- sub-domain skill; run Is-A / Has-A and SOLID gate before any new inheritance in C++ code