irahardianto / cpp-idioms
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
- RAII — resources tied to object lifetime. No manual
new/delete. std::unique_ptrfor exclusive ownership,std::shared_ptronly when sharing is necessary.- Move semantics — prefer moving over copying for expensive types.
Rule of Five/Zero:
`cpp
// ✅ Rule of Zero — let compiler-generated defaults work
class TaskService {
std::uniqueptr<TaskStorage> storage;
std::sharedptr<Logger> logger;
public:
// No destructor, copy/move constructors needed — smart pointers handle it
explicit TaskService(std::uniqueptr<TaskStorage> storage, std::sharedptr<Logger> logger)
: storage(std::move(storage)), logger(std::move(logger)) {}
};// ✅ Rule of Five — when managing raw resources (rare)
class Buffer {
char* data_;
sizet 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
};`Pass by value and move for sink parameters:
`cpp
// ✅ Sink parameter — takes ownership
void addTask(Task task) {
tasks.pushback(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.
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);`- Exceptions for exceptional conditions only. Never for control flow.
noexcepton move constructors and destructors.Domain error types:
`cpp
enum class TaskError {
NotFound,
ValidationFailed,
StorageUnavailable,
};// ✅ For exception-based paths
class TaskNotFoundException : public std::runtime_error {
std::string taskid;
public:
explicit TaskNotFoundException(std::string id)
: std::runtimeerror("Task not found: " + id), taskid_(std::move(id)) {}
const std::string& taskid() const noexcept { return taskid_; }
};`
Modern Features
std::string_viewfor read-only string parameters (no allocation):`cpp
// ✅ Zero-copy string parameter
void log(std::stringview message, std::stringview context);// ❌ Unnecessary copy
void log(const std::string& message);`constexprfor 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);
}`- Structured bindings:
auto [id, title] = getTask(); std::optionalfor 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);
}`std::variantfor 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);`Concepts (C++20) for constrained templates:
`cpp
template<typename T>
concept Storable = requires(T t) {
{ t.id() } -> std::convertibleto<std::stringview>;
{ t.serialize() } -> std::same_as<std::vector<char>>;
};template<Storable T>
void save(const T& entity);`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
// ✅ 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
- PascalCase for types. camelCase or snake_case for functions (project-consistent).
_suffix for private members:storage_,logger_.- No Hungarian notation.
- ALL_CAPS only for macros — never for constants (use
constexpr).
Anti-Patterns
- ❌ Raw
new/delete— usestd::make_unique/std::make_shared - ❌
using namespace std;in headers — pollutes global namespace - ❌ Catching
...without re-throwing — swallows all errors including crashes - ❌
const_castto remove constness — design flaw, fix the interface - ❌
reinterpret_castwithout safety justification — undefined behavior risk - ❌ C-style casts
(int)x— usestatic_cast<int>(x)for type safety - ❌
std::shared_ptrwhenstd::unique_ptrsuffices — unnecessary overhead ❌ Copy-heavy loops without
const auto&:// ❌ 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.
// 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