| 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) { }
};