| name | programming-cpp |
| description | C++ programming skill based on C++ Core Guidelines - use for implementing C++ code |
C++ Programming Skill
Use this skill when writing or modifying C++ code.
Follow the [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) as the primary reference for best practices.
C++17 Standard. This project uses C++17. Use only features available in C++17. Do NOT use C++20 features (concepts, ranges, std::span, etc.).
Compile-time execution is a PRIORITY. If code CAN be executed at compile time, it SHOULD be. Use constexpr, if constexpr, template metaprogramming. Move as much computation as possible from runtime to compile time.
Performance is critical. This is performance-sensitive software. Always consider performance implications. Prefer zero-cost abstractions, avoid unnecessary allocations, and be cache-friendly.
All code MUST be unit testable. Use Dependency Injection and/or Policy-based design to achieve testability.
Guidelines override existing code style. If you see existing code that violates the rules in this skill, apply these guidelines first and ignore the current project style. Do NOT propagate bad patterns just because they exist in the codebase.
Philosophy (from C++ Core Guidelines)
- P.1: Express ideas directly in code
- P.2: Write in ISO Standard C++
- P.3: Express intent
- P.4: Ideally, a program should be statically type safe
- P.5: Prefer compile-time checking to run-time checking
- P.6: What cannot be checked at compile time should be checkable at run time
- P.9: Don't waste time or space
- P.10: Prefer immutable data to mutable data
- P.11: Encapsulate messy constructs, rather than spreading through the code
Interfaces
- I.1: Make interfaces explicit
- I.2: Avoid non-const global variables
- I.3: Avoid singletons
- I.4: Make interfaces precisely and strongly typed
- I.11: Never transfer ownership by a raw pointer (
T*) or reference (T&)
- I.13: Do not pass an array as a single pointer
Functions
- F.1: "Package" meaningful operations as carefully named functions
- F.2: A function should perform a single logical operation
- F.3: Keep functions short and simple
- F.4: If a function might have to be evaluated at compile time, declare it
constexpr
- F.6: If your function must not throw, declare it
noexcept
- F.7: For general use, take
T* or T& arguments rather than smart pointers
- F.15: Prefer simple and conventional ways of passing information
- F.16: For "in" parameters, pass cheaply-copied types by value and others by reference to
const
- F.17: For "in-out" parameters, pass by reference to non-
const
- F.20: For "out" output values, prefer return values to output parameters
- F.21: To return multiple "out" values, prefer returning a struct or tuple
- F.26: Use a
unique_ptr<T> to transfer ownership where a pointer is needed
- F.27: Use a
shared_ptr<T> to share ownership
Classes and Class Hierarchies
- C.1: Organize related data into structures (
structs or classes)
- C.2: Use
class if the class has an invariant; use struct if the data members can vary independently
- C.4: Make a function a member only if it needs direct access to the representation of a class
- C.7: Don't define a class or enum and declare a variable of its type in the same statement
- C.9: Minimize exposure of members
- C.20: If you can avoid defining default operations, do
- C.21: If you define or
=delete any copy, move, or destructor function, define or =delete them all (Rule of 0/5)
- C.35: A base class destructor should be either public and virtual, or protected and non-virtual
- C.47: Define and initialize data members in the order of member declaration
- C.48: Prefer default member initializers to member initializers in constructors for constant initializers
- C.49: Prefer initialization to assignment in constructors
- C.80: Use
=default if you have to be explicit about using the default semantics
- C.82: Don't call virtual functions in constructors and destructors
- C.128: Virtual functions should specify exactly one of
virtual, override, or final
- C.131: Avoid trivial getters and setters
- C.149: Use
unique_ptr or shared_ptr to avoid forgetting to delete objects created using new
Resource Management
- R.1: Manage resources automatically using resource handles and RAII
- R.2: In interfaces, use raw pointers to denote individual objects (only)
- R.3: A raw pointer (
T*) is non-owning
- R.4: A raw reference (
T&) is non-owning
- R.5: Prefer scoped objects, don't heap-allocate unnecessarily
- R.10: Avoid
malloc() and free()
- R.11: Avoid calling
new and delete explicitly
- R.12: Immediately give the result of an explicit resource allocation to a manager object
- R.13: Perform at most one explicit resource allocation in a single expression statement
- R.20: Use
unique_ptr or shared_ptr to represent ownership
- R.21: Prefer
unique_ptr over shared_ptr unless you need to share ownership
- R.22: Use
make_shared() to make shared_ptrs
- R.23: Use
make_unique() to make unique_ptrs
Error Handling
- E.1: Develop an error-handling strategy early in a design
- E.2: Throw an exception to signal that a function can't perform its assigned task
- E.3: Use exceptions for error handling only
- E.6: Use RAII to prevent leaks
- E.13: Never throw while being the direct owner of an object
- E.14: Use purpose-designed user-defined types as exceptions (not built-in types)
- E.16: Destructors, deallocation,
swap, and exception type copy/move construction must never fail
- E.17: Don't try to catch every exception in every function
- E.18: Minimize the use of explicit
try/catch
- E.25: If you can't throw exceptions, simulate RAII for resource management
Performance (CRITICAL)
This is performance-critical software. Apply these rules from C++ Core Guidelines - Performance:
Core Performance Rules
- Per.1: Don't optimize without reason
- Per.2: Don't optimize prematurely
- Per.3: Don't optimize something that's not performance critical
- Per.4: Don't assume that complicated code is necessarily faster than simple code
- Per.5: Don't assume that low-level code is necessarily faster than high-level code
- Per.6: Don't make claims about performance without measurements
- Per.7: Design to enable optimization
- Per.10: Rely on the static type system
- Per.11: Move computation from run time to compile time
- Per.19: Access memory predictably (cache-friendly)
Memory & Allocation
void process_items(const std::vector<item>& items) {
for (const auto& item : items) {
auto result = std::make_unique<result_t>();
}
}
void process_items(const std::vector<item>& items) {
result_t result;
for (const auto& item : items) {
result.reset();
}
}
std::vector<int> results;
results.reserve(items.size());
Avoid Unnecessary Copies
void process(std::vector<int> data) { }
void process(const std::vector<int>& data) { }
void take_ownership(std::vector<int> data) {
m_data = std::move(data);
}
const std::string& get_name() { return m_name; }
const std::string& bad() { return std::string("temp"); }
std::string get_computed_name() {
std::string result = compute();
return result;
}
Move Semantics
std::vector<int> source = get_data();
process(std::move(source));
class heavy_resource {
public:
heavy_resource(heavy_resource&& other) noexcept
: m_data(std::exchange(other.m_data, nullptr)) {}
heavy_resource& operator=(heavy_resource&& other) noexcept {
if (this != &other) {
delete m_data;
m_data = std::exchange(other.m_data, nullptr);
}
return *this;
}
};
Cache-Friendly Code
struct node {
node* next;
int data;
};
std::vector<int> data;
for (int col = 0; col < cols; ++col)
for (int row = 0; row < rows; ++row)
matrix[row][col] = 0;
for (int row = 0; row < rows; ++row)
for (int col = 0; col < cols; ++col)
matrix[row][col] = 0;
Compile-Time vs Runtime
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
constexpr int fact_10 = factorial(10);
template<typename T>
void process(T value) {
if constexpr (std::is_integral_v<T>) {
} else {
}
}
template<typename Handler>
void process(Handler& h) { h.handle(); }
Avoid Performance Pitfalls
| Pitfall | Solution |
|---|
std::endl in loops | Use '\n' (no flush) |
std::map for small sets | Use std::vector + linear search for < 20 elements |
| String concatenation in loops | Use std::ostringstream or pre-reserve |
Unnecessary shared_ptr | Use unique_ptr (no atomic refcount) |
| Virtual calls in hot loops | Use CRTP or templates |
std::function in hot path | Use templates or function pointers |
| Exceptions for control flow | Use return values or std::optional |
| Copying in range-for | Use const auto& |
Performance Checklist
Testability (REQUIRED)
All C++ code must be designed for unit testing. Use these patterns:
Dependency Injection (Runtime)
Inject dependencies through constructor or setter instead of creating them internally:
class order_processor {
database m_db;
public:
void process(const order& o) {
m_db.save(o);
}
};
class i_database {
public:
virtual ~i_database() = default;
virtual void save(const order& o) = 0;
};
class order_processor {
i_database& m_db;
public:
explicit order_processor(i_database& db) : m_db(db) {}
void process(const order& o) {
m_db.save(o);
}
};
class mock_database : public i_database {
public:
void save(const order& o) override { }
};
Policy-Based Design (Compile-time)
Use templates with policy classes for compile-time dependency injection:
template<typename DatabasePolicy>
class order_processor {
DatabasePolicy m_db;
public:
void process(const order& o) {
m_db.save(o);
}
};
struct production_database {
void save(const order& o) { }
};
struct mock_database {
void save(const order& o) { }
};
using prod_processor = order_processor<production_database>;
using test_processor = order_processor<mock_database>;
When to Use Which
| Pattern | Use When... |
|---|
| Dependency Injection | Need runtime polymorphism, plugins, or when interface is natural |
| Policy-Based Design | Need maximum performance (no virtual calls), behavior known at compile time |
| Both | Complex systems may combine both approaches |
Testability Checklist
Code Style & Attributes (REQUIRED)
Early Return
Use early return to reduce nesting and improve readability:
std::optional<result> process(const input& in) {
if (in.is_valid()) {
if (auto data = fetch_data(in)) {
if (data->size() > 0) {
return compute(*data);
}
}
}
return std::nullopt;
}
std::optional<result> process(const input& in) {
if (!in.is_valid())
return std::nullopt;
auto data = fetch_data(in);
if (!data)
return std::nullopt;
if (data->empty())
return std::nullopt;
return compute(*data);
}
const Correctness
Mark everything const that doesn't need to change:
const int max_size = 100;
const auto& item = container[0];
void process(const std::string& input);
class data_holder {
public:
int get_value() const { return m_value; }
const std::string& get_name() const { return m_name; }
private:
int m_value;
std::string m_name;
};
const int* ptr_to_const;
int* const const_ptr;
const int* const both_const;
constexpr
Use constexpr for compile-time evaluation:
constexpr int max_buffer_size = 1024;
constexpr double pi = 3.14159265359;
constexpr int square(int x) { return x * x; }
constexpr int size = square(10);
struct point {
constexpr point(int x, int y) : m_x(x), m_y(y) {}
constexpr int x() const { return m_x; }
constexpr int y() const { return m_y; }
private:
int m_x, m_y;
};
constexpr point origin{0, 0};
noexcept
Mark functions noexcept when they don't throw:
class resource {
public:
resource(resource&& other) noexcept;
resource& operator=(resource&& other) noexcept;
};
~resource() noexcept;
void swap(resource& other) noexcept;
int get_size() const noexcept { return m_size; }
template<typename T>
void process(T& obj) noexcept(noexcept(obj.do_work())) {
obj.do_work();
}
[[nodiscard]]
Use [[nodiscard]] when ignoring return value is likely a bug:
[[nodiscard]] std::unique_ptr<widget> create_widget();
[[nodiscard]] error_code save_file(const std::string& path);
[[nodiscard]] bool try_lock();
[[nodiscard]] iterator find(const key_type& key);
class [[nodiscard]] result {
};
[[maybe_unused]]
Use [[maybe_unused]] to suppress warnings for intentionally unused variables:
void log_message([[maybe_unused]] const char* file,
[[maybe_unused]] int line,
const std::string& message) {
#ifdef DEBUG
std::cerr << file << ":" << line << ": " << message << "\n";
#else
std::cerr << message << "\n";
#endif
}
void process(const data& d) {
[[maybe_unused]] bool valid = validate(d);
assert(valid);
}
void critical_section() {
[[maybe_unused]] std::lock_guard lock(m_mutex);
}
Attributes Summary
| Attribute | Use When |
|---|
const | Value/object won't be modified |
constexpr | Can be evaluated at compile time |
noexcept | Function guaranteed not to throw |
[[nodiscard]] | Ignoring return value is likely a bug |
[[maybe_unused]] | Intentionally unused (suppress warning) |
C++17 Features (USE THESE)
This project uses C++17. Use these features:
Type Deduction & Inference
auto for type inference when type is obvious
- Structured bindings:
auto [key, value] = map.begin();
- Class template argument deduction:
std::pair p{1, 2.0};
Control Flow
if constexpr for compile-time branching (PRIORITY!)
if/switch with initializer: if (auto it = map.find(key); it != map.end())
Standard Library
std::optional<T> for optional values (no more nullptr/sentinel)
std::variant<Ts...> for type-safe unions
std::string_view for read-only string parameters (zero-copy!)
std::any for type-safe void* (use sparingly)
std::invoke for uniform callable invocation
std::apply for tuple unpacking to function calls
Compile-Time (PRIORITY)
constexpr if - compile-time branching
constexpr lambdas - lambdas can be constexpr
inline variables - define variables in headers
- Fold expressions -
(args + ...) for variadic templates
Attributes
[[nodiscard]] - warn if return value ignored
[[maybe_unused]] - suppress unused warnings
[[fallthrough]] - intentional switch fallthrough
Filesystem
std::filesystem - portable path/file operations
Parallel Algorithms
- Execution policies:
std::execution::par, std::execution::seq
auto [iter, success] = map.insert({key, value});
if constexpr (std::is_integral_v<T>) {
}
std::optional<int> find_value(int key);
std::string_view get_name();
NOT Available (C++20+) - DO NOT USE
std::span - scan project for existing span implementation, otherwise use pointer + size
Concepts - use SFINAE or static_assert
Ranges - use STL algorithms
std::format - use fmt library or streams (if available in project)
Coroutines - not available
[[nodiscard("message")]] - use [[nodiscard]] without message
gsl::span - GSL library is NOT used in this project
For span-like functionality:
- First, search the codebase for existing span implementation (e.g.,
span, array_view, buffer_view)
- If found, use the project's existing implementation
- If not found, use pointer + size parameters:
void process(const T* data, size_t size);
template<typename Iterator>
void process(Iterator begin, Iterator end);
Naming Conventions
Follow project conventions. If none exist, use:
snake_case for functions, variables, namespaces
PascalCase for types (classes, structs, enums, type aliases)
SCREAMING_SNAKE_CASE for macros and constants
m_ or _ prefix for member variables
- Avoid Hungarian notation
Documentation & Comments
Doxygen Style (REQUIRED)
Use Doxygen-style comments for all public APIs:
int add(int a, int b) {
return a + b;
}
class connection {
public:
void connect(std::string_view host, uint16_t port);
[[nodiscard]] size_t send(const void* data, size_t size);
};
Common Doxygen Tags
| Tag | Usage |
|---|
@param name | Document a function parameter |
@return | Document return value |
@throws exception | Document exceptions thrown |
@note | Additional notes |
@warning | Important warnings |
@see | Cross-reference to related items |
@deprecated | Mark as deprecated |
@code / @endcode | Code example block |
Documentation vs. inline comments
Two distinct things:
| Kind | Purpose | Form | Lives |
|---|
| Documentation | Tells callers how to use this API | Doxygen block, javadoc style (/** @param @return @throws */) | Above public APIs |
| Inline comment | Explains a non-obvious line | // short why | Beside the line, same indentation |
Code that needs neither is the goal. Reach for them only when the code by itself does not answer a question a competent reader will have.
When to write Doxygen documentation
Add a /** */ block when ANY of these is true and the answer is not in the function name + signature:
- The function is part of a public API (header consumed by other modules / packages).
- Units, ownership, or threading are non-obvious (e.g. "nanoseconds, monotonic", "caller takes ownership", "callable from any thread").
- The function can fail in ways the type does not advertise (
@throws, "returns std::nullopt on FOO").
- Pre/post-conditions exist beyond the type system (e.g. "input must be sorted").
Use exactly the javadoc-style tags already documented above (@param, @return, @throws, @note, @warning, @see, @deprecated).
When to write an inline comment
Add a // ... line ONLY when ALL of these hold:
- The reader cannot infer intent from the code itself.
- The information has long-term value (still true a year from now, after refactors).
- The information is not already in the commit message, the PR description, the bug tracker, or another file.
If any one fails, drop the comment.
Good comments
i++;
timeout *= 2;
buffer.reserve(1024);
result |= 0x80;
{
state.value = compute(...);
}
auto handle = legacy_open(path, true);
Each one carries information the code does not: a hidden invariant, a non-obvious algorithm choice, a workaround tagged to a ticket, a domain quirk.
Bad comments
i++;
int sum = a + b;
std::vector<int> numbers;
result.clear();
for (auto& item : items) {
process(item);
}
m_count = 0;
Each one restates what the code already says. Delete.
Obvious things - no comment needed
These need neither documentation nor inline comments:
- Trivial getters / setters:
int size() const { return m_size; }
- Self-descriptive predicates:
bool is_empty() const, bool has_value()
- Standard idioms the language defines:
std::move(x), std::make_unique<T>()
- Operators with the conventional meaning (
operator==, operator+)
- Constructors / destructors that do exactly what RAII implies
- Internal helpers used in one place with a name that already says what they do
- Unit-test functions: the
TEST(suite, name) already names the case
size_t size() const { return m_size; }
size_t size() const { return m_size; }
Long-form blocks: when to delete
Multi-line // ... blocks at the top of files, above test cases, or between helpers tend to re-tell story already in the diff, the test name, the PR description, or the commit message. Strip them by default.
Delete:
- "Background. ..." / "Test strategy. ..." preambles in test files. Test name + 1-line note above the suite is enough.
- "Site 1 / Site 2 / Site 3" banners separating helpers whose names already say which site they mirror.
- Comments that cite
file:line in another file. The line numbers rot; the PR description owns cross-file context.
- "This used to be broken because X" comments above the fix. The commit message owns that.
- Decorative section banners like
// === Helpers ===.
Keep:
- Hidden invariants ("caller holds m_mutex", "must be called once at startup").
- Workarounds with a ticket reference and a sunset condition.
- Unit / ownership / threading notes that the type system does not enforce.
- Doxygen blocks on public APIs.
Rule of thumb: if removing the comment would not confuse a competent reader a year from now, it is noise.
Eliminating if/else Branching
Long if/else chains are a code smell. Replace with these patterns:
When to Refactor
| Smell | Threshold |
|---|
| if/else chain | 3+ branches |
| switch statement | 5+ cases |
| Deep nesting | 3+ levels |
| Type-based branching | Any dynamic_cast chain |
Pattern 1: Polymorphism (Strategy/State)
Use when: Behavior varies by type, need extensibility.
void process(int type, Data& data) {
if (type == 1) {
handleType1(data);
} else if (type == 2) {
handleType2(data);
} else if (type == 3) {
handleType3(data);
}
}
class Handler {
public:
virtual ~Handler() = default;
virtual void process(Data& data) = 0;
};
class Type1Handler : public Handler {
void process(Data& data) override { }
};
void process(Handler& handler, Data& data) {
handler.process(data);
}
Pattern 2: Lookup Table / Map
Use when: Mapping values to values, simple dispatch.
std::string getStatusText(int code) {
if (code == 200) return "OK";
else if (code == 404) return "Not Found";
else if (code == 500) return "Server Error";
return "Unknown";
}
const std::unordered_map<int, std::string> STATUS_TEXT = {
{200, "OK"},
{404, "Not Found"},
{500, "Server Error"}
};
std::string getStatusText(int code) {
auto it = STATUS_TEXT.find(code);
return it != STATUS_TEXT.end() ? it->second : "Unknown";
}
Pattern 3: Command Map (Function Dispatch)
Use when: Different actions based on command/type.
void execute(const std::string& cmd, Context& ctx) {
if (cmd == "start") start(ctx);
else if (cmd == "stop") stop(ctx);
else if (cmd == "pause") pause(ctx);
else if (cmd == "resume") resume(ctx);
}
using CommandFn = std::function<void(Context&)>;
const std::unordered_map<std::string, CommandFn> COMMANDS = {
{"start", [](Context& ctx) { start(ctx); }},
{"stop", [](Context& ctx) { stop(ctx); }},
{"pause", [](Context& ctx) { pause(ctx); }},
{"resume", [](Context& ctx) { resume(ctx); }}
};
void execute(const std::string& cmd, Context& ctx) {
if (auto it = COMMANDS.find(cmd); it != COMMANDS.end()) {
it->second(ctx);
}
}
Pattern 4: std::variant + std::visit
Use when: Type-safe union, different handling per type.
double getArea(Shape* shape) {
if (auto* c = dynamic_cast<Circle*>(shape)) {
return 3.14159 * c->radius * c->radius;
} else if (auto* r = dynamic_cast<Rectangle*>(shape)) {
return r->width * r->height;
} else if (auto* t = dynamic_cast<Triangle*>(shape)) {
return 0.5 * t->base * t->height;
}
return 0;
}
using Shape = std::variant<Circle, Rectangle, Triangle>;
double getArea(const Shape& shape) {
return std::visit([](const auto& s) -> double {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Circle>) {
return 3.14159 * s.radius * s.radius;
} else if constexpr (std::is_same_v<T, Rectangle>) {
return s.width * s.height;
} else {
return 0.5 * s.base * s.height;
}
}, shape);
}
Pattern 5: Early Return (Guard Clauses)
Use when: Validation or precondition checks cause deep nesting.
Result process(Request* req) {
if (req != nullptr) {
if (req->isValid()) {
if (req->hasPermission()) {
if (req->data.size() > 0) {
return doActualWork(req);
}
}
}
}
return Result::Error;
}
Result process(Request* req) {
if (!req) return Result::Error;
if (!req->isValid()) return Result::InvalidRequest;
if (!req->hasPermission()) return Result::Forbidden;
if (req->data.empty()) return Result::EmptyData;
return doActualWork(req);
}
Pattern 6: Null Object
Use when: Eliminating null checks throughout code.
void logMessage(Logger* logger, const std::string& msg) {
if (logger != nullptr) {
logger->log(msg);
}
}
class NullLogger : public Logger {
void log(const std::string&) override { }
};
Logger& getLogger() {
static NullLogger nullLogger;
return currentLogger ? *currentLogger : nullLogger;
}
void logMessage(Logger& logger, const std::string& msg) {
logger.log(msg);
}
Pattern 7: Template + if constexpr
Use when: Compile-time type-based branching.
void serialize(const std::any& value, std::ostream& out) {
if (value.type() == typeid(int)) {
out << std::any_cast<int>(value);
} else if (value.type() == typeid(std::string)) {
out << std::any_cast<std::string>(value);
}
}
template<typename T>
void serialize(const T& value, std::ostream& out) {
if constexpr (std::is_integral_v<T>) {
out << value;
} else if constexpr (std::is_same_v<T, std::string>) {
out << '"' << value << '"';
} else {
static_assert(always_false<T>, "Unsupported type");
}
}
Pattern Summary
| Pattern | Best For | Overhead |
|---|
| Polymorphism | Extensible behavior, OOP design | Virtual call |
| Lookup table | Value mapping | Map lookup |
| Command map | Action dispatch | Map + function call |
| std::variant | Type-safe unions, closed set | Visit overhead |
| Early return | Validation, preconditions | None |
| Null Object | Eliminating null checks | None |
| if constexpr | Compile-time branching | None (compile-time) |
When NOT to Refactor
Keep simple if/else when:
- Only 2 branches
- Logic is truly simple and clear
- Refactoring would add complexity without benefit
- Performance-critical hot path where map lookup is slower than branch
Code Style Checklist
Before submitting C++ code:
Correctness & Safety
Performance
Testability
Style & Documentation
References