- name
- cpp-idioms
- description
- Modern C++ (17/20/23) idioms: RAII, smart pointers, value semantics, Rule of Zero/Five, concepts, and std::expected error handling. Use when writing, refactoring, or reviewing C++ code, CMake targets, or native modules.
## C++ Idioms and Patterns
Modern C++ (17/20/23) rewards RAII, value semantics, and zero-cost abstractions. Lean into the type system and smart pointers. Idiomatic C++ = safe, deterministic, performant.
> Scope: C++ coding idioms. Test naming: .agents/rules/testing-strategy.md.
### Ownership and Memory
1. **RAII — resources tied to object lifetime. No manual `new`/`delete`.**
2. **`std::unique_ptr` for exclusive ownership, `std::shared_ptr` only when sharing is necessary.**
3. **Move semantics — prefer moving over copying for expensive types.**
4. **Rule of Five/Zero:**
```cpp
// ✅ Rule of Zero — let compiler-generated defaults work
class TaskService {
std::unique_ptr<TaskStorage> storage_;
std::shared_ptr<Logger> logger_;
public:
// No destructor, copy/move constructors needed — smart pointers handle it
explicit TaskService(std::unique_ptr<TaskStorage> storage, std::shared_ptr<Logger> logger)
: storage_(std::move(storage)), logger_(std::move(logger)) {}
};
// ✅ Rule of Five — when managing raw resources (rare)
class Buffer {
char* data_;
size_t size_;
public:
~Buffer(); // Destructor
Buffer(const Buffer&); // Copy constructor
Buffer& operator=(const Buffer&); // Copy assignment
Buffer(Buffer&&) noexcept; // Move constructor
Buffer& operator=(Buffer&&) noexcept; // Move assignment
};
```
5. **Pass by value and move for sink parameters:**
```cpp
// ✅ Sink parameter — takes ownership
void addTask(Task task) {
tasks_.push_back(std::move(task));
}
// ✅ Read-only — const reference
void printTask(const Task& task);
// ❌ Never pass smart pointers unless transferring ownership
void processTask(std::shared_ptr<Task> task); // Unnecessary ref-count bump
// ✅ If function doesn't need ownership
void processTask(const Task& task);
```
### Error Handling
> For universal error handling principles, see `.agents/rules/error-handling-principles.md`. Below: language-specific patterns only.
1. **`std::expected` (C++23) or Result types for expected failures:**
```cpp
// ✅ C++23 std::expected
std::expected<Task, TaskError> getTask(std::string_view id) {
auto it = tasks_.find(id);
if (it == tasks_.end()) {
return std::unexpected(TaskError::NotFound);
}
return it->second;
}
// Caller handles both paths
auto result = getTask("123");
if (!result) {
handleError(result.error());
return;
}
processTask(*result);
```
2. **Exceptions for exceptional conditions only.** Never for control flow.
3. **`noexcept` on move constructors and destructors.**
4. **Domain error types:**
```cpp
enum class TaskError {
NotFound,
ValidationFailed,
StorageUnavailable,
};
// ✅ For exception-based paths
class TaskNotFoundException : public std::runtime_error {
std::string task_id_;
public:
explicit TaskNotFoundException(std::string id)
: std::runtime_error("Task not found: " + id), task_id_(std::move(id)) {}
const std::string& task_id() const noexcept { return task_id_; }
};
```
### Modern Features
1. **`std::string_view`** for read-only string parameters (no allocation):
```cpp
// ✅ Zero-copy string parameter
void log(std::string_view message, std::string_view context);
// ❌ Unnecessary copy
void log(const std::string& message);
```
2. **`constexpr`** for compile-time evaluation:
```cpp
constexpr int maxRetries = 3;
constexpr auto timeout = std::chrono::seconds(30);
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
```
3. **Structured bindings**: `auto [id, title] = getTask();`
4. **`std::optional`** for nullable values:
```cpp
// ✅ Explicit nullability
std::optional<Task> findById(std::string_view id);
// Caller checks explicitly
if (auto task = findById("123"); task.has_value()) {
process(*task);
}
```
5. **`std::variant`** for type-safe unions (sum types):
```cpp
using TaskResult = std::variant<Task, TaskError>;
TaskResult result = getTask(id);
std::visit(overloaded{
[](const Task& task) { process(task); },
[](TaskError err) { handleError(err); },
}, result);
```
6. **Concepts (C++20)** for constrained templates:
```cpp
template<typename T>
concept Storable = requires(T t) {
{ t.id() } -> std::convertible_to<std::string_view>;
{ t.serialize() } -> std::same_as<std::vector<char>>;
};
template<Storable T>
void save(const T& entity);
```
7. **Ranges (C++20)** for functional-style pipelines:
```cpp
auto active_titles = tasks
| std::views::filter([](const Task& t) { return t.is_active(); })
| std::views::transform(&Task::title)
| std::views::take(10);
```
### Interfaces and DI
```cpp
// ✅ Interface (abstract class with pure virtuals)
class TaskStorage {
public:
virtual ~TaskStorage() = default;
virtual std::expected<Task, TaskError> getById(std::string_view id) = 0;
virtual void save(const Task& task) = 0;
};
// ✅ Production implementation
class PostgresTaskStorage : public TaskStorage {
// ...
};
// ✅ Test implementation
class MockTaskStorage : public TaskStorage {
// MOCK_METHOD from Google Mock, or hand-rolled
};
// ✅ Constructor injection
class TaskService {
std::unique_ptr<TaskStorage> storage_;
public:
explicit TaskService(std::unique_ptr<TaskStorage> storage)
: storage_(std::move(storage)) {}
};
```
### Naming
1. **PascalCase** for types. **camelCase** or **snake_case** for functions (project-consistent).
2. **`_` suffix** for private members: `storage_`, `logger_`.
3. No Hungarian notation.
4. **ALL_CAPS** only for macros — never for constants (use `constexpr`).
### Anti-Patterns
- ❌ **Raw `new`/`delete`** — use `std::make_unique` / `std::make_shared`
- ❌ **`using namespace std;`** in headers — pollutes global namespace
- ❌ **Catching `...` without re-throwing** — swallows all errors including crashes
- ❌ **`const_cast` to remove constness** — design flaw, fix the interface
- ❌ **`reinterpret_cast` without safety justification** — undefined behavior risk
- ❌ **C-style casts `(int)x`** — use `static_cast<int>(x)` for type safety
- ❌ **`std::shared_ptr` when `std::unique_ptr` suffices** — unnecessary overhead
- ❌ **Copy-heavy loops without `const auto&`:**
```cpp
// ❌ Copies every Task
for (auto task : tasks) { process(task); }
// ✅ Reference — no copy
for (const auto& task : tasks) { process(task); }
```
### Testing
Google Test or Catch2. Google Mock for interface mocking.
```cpp
// Google Test + Google Mock
TEST(TaskServiceTest, ReturnsNotFoundForMissingTask) {
auto storage = std::make_unique<MockTaskStorage>();
EXPECT_CALL(*storage, getById("999"))
.WillOnce(Return(std::unexpected(TaskError::NotFound)));
TaskService service(std::move(storage));
auto result = service.getTask("999");
EXPECT_FALSE(result.has_value());
EXPECT_EQ(result.error(), TaskError::NotFound);
}
```
### Formatting and Static Analysis
| Tool | Purpose | Command |
|---|---|---|
| `clang-format` | Formatting | `clang-format -i src/**/*.cpp` |
| `clang-tidy` | Static analysis | `clang-tidy src/*.cpp --` |
| `cppcheck` | Bug detection | `cppcheck --enable=all src/` |
| AddressSanitizer | Memory errors | `-fsanitize=address` |
| ThreadSanitizer | Race conditions | `-fsanitize=thread` |
| UBSanitizer | Undefined behavior | `-fsanitize=undefined` |
### Related
- Code Idioms and Conventions .agents/rules/code-idioms-and-conventions.md
- Resources and Memory Management @.agents/rules/resources-and-memory-management-principles.md
- Concurrency and Threading Principles @.agents/rules/concurrency-and-threading-principles.md
- Error Handling Principles @.agents/rules/error-handling-principles.md
Voir sur GitHub