cpp-development
Develop, review, and modernize idiomatic C++ applications (C++17/C++20/C++23) following the C++ Core Guidelines and RAII principles. Use when writing modern C++ classes, managing lifetimes with smart
SKILL.mdskills/cpp-development/SKILL.md
GitHubModern Idiomatic C++ Development
Modern C++ (C++17, C++20, C++23) emphasizes Resource Acquisition Is Initialization (RAII), strict type safety, value semantics, and zero-cost abstractions. Avoid legacy C-style idioms such as raw new/delete, raw C-arrays, and manual error return codes.
Detailed reference guides:
- RAII & Smart Pointer Lifetimes
- Modern C++ Standards (C++17 to C++23)
- C++ Core Guidelines & Exception Safety
Core Rules
- RAII is mandatory: Tie every system resource (heap memory, file handles, mutex locks, sockets) to the lifetime of an object on the stack.
- No manual
newordelete: Usestd::make_unique<T>()andstd::make_shared<T>(). - Follow the Rule of Zero: Design classes with member types that manage their own resources (e.g.
std::string,std::vector,std::unique_ptr) so you don't need custom copy/move constructors or destructors. - Const correctness: Mark member functions
constif they do not modify observable state. Mark variablesconstorconstexprby default. - Pass large objects by
const&or by value withstd::move: Never copy large collections unnecessarily. - Use modern CMake targets: Avoid global commands like
include_directoriesorlink_libraries. Always usetarget_include_directories,target_link_libraries, and target-based properties.
1. Resource Management: RAII and Smart Pointers
#include <memory>
#include <string>
#include <fstream>
#include <stdexcept>
class FileLogger {
public:
explicit FileLogger(const std::string& filepath)
: file_stream_(filepath, std::ios::app) {
if (!file_stream_.is_open()) {
throw std::runtime_error("Failed to open log file: " + filepath);
}
}
// Default destructor automatically closes file_stream_ (Rule of Zero)
~FileLogger() = default;
void log(std::string_view message) {
file_stream_ << message << '\n';
}
private:
std::ofstream file_stream_;
};
// Creating polymorphic objects with exclusive ownership
void process_logging() {
auto logger = std::make_unique<FileLogger>("app.log");
logger->log("Service initialized successfully.");
// logger automatically freed when going out of scope - even if an exception is thrown!
}
2. Value Semantics and Move Semantics
Leverage move semantics to transfer expensive heap buffers without deep copies:
#include <vector>
#include <string>
#include <utility>
class Document {
public:
// Accept by value and move into internal storage
Document(std::string title, std::vector<std::string> lines)
: title_(std::move(title)), lines_(std::move(lines)) {}
[[nodiscard]] const std::string& title() const noexcept { return title_; }
[[nodiscard]] const std::vector<std::string>& lines() const noexcept { return lines_; }
private:
std::string title_;
std::vector<std::string> lines_;
};
3. Modern Concepts (C++20) vs Template SFINAE
Constrain template parameters with expressive concepts instead of complex std::enable_if_t:
#include <concepts>
#include <iostream>
template <typename T>
concept Printable = requires(T a) {
{ std::cout << a } -> std::same_as<std::ostream&>;
};
template <Printable T>
void print_value(const T& val) {
std::cout << "Value: " << val << '\n';
}
4. Modern Target-Based CMake
cmake_minimum_required(VERSION 3.20)
project(EngineApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(engine_core
src/engine.cpp
include/engine/engine.hpp
)
target_include_directories(engine_core PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_options(engine_core PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Werror>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
)
add_executable(app_cli src/main.cpp)
target_link_libraries(app_cli PRIVATE engine_core)
5. Common Mistakes and Anti-Patterns
| Anti-Pattern | Risk | Correct Practice |
|---|---|---|
T* ptr = new T() |
Memory leak on early return or exception | Use auto ptr = std::make_unique<T>() |
Overusing std::shared_ptr |
Overhead of atomic reference counting & circular leaks | Default to std::unique_ptr; share only when true shared ownership exists |
Catching exceptions by value: catch (std::exception e) |
Slicing bug (drops derived type details) | Catch by const reference: catch (const std::exception& e) |
| Calling virtual functions in constructors/destructors | Calls base class version, not derived (dynamic dispatch not active) | Avoid virtual calls during object construction/destruction |
Missing [[nodiscard]] on pure getters/creators |
Callers discard results, leading to silent bugs | Mark pure queries with [[nodiscard]] |
6. Verification Checklist
- Zero manual
newordeleteoperators in codebase. - Destructors in polymorphic base classes are
virtual(virtual ~Base() = default;). - Passed
-Wall -Wextra -Wpedanticwithout compiler warnings. - Tested with AddressSanitizer (
-fsanitize=address,undefined). - No circular references in
std::shared_ptrgraphs (usestd::weak_ptrfor back-references).