This guide outlines modern C++ development practices emphasizing the upcoming C++26 (C++2c) standard, along with comprehensive project structure conventions, build systems, and testing frameworks.
C++ Language Standards
This guide covers modern C++ development practices with C++26 (C++2c) as the target standard. While the conventions and best practices apply across recent C++ versions, we highlight several key C++26 features that represent the cutting edge of the language. C++26 brings powerful capabilities that make code more expressive, safer, and easier to maintain. By adopting these practices early, you position your codebase to take advantage of evolving language capabilities and stay current with industry best practices.
Key C++26 Features
C++26 introduces several notable improvements. While this guide doesn't cover every C++26 addition, the following features demonstrate significant advances in expressiveness and safety:
Compile-Time Programming and constexpr (C++11, enhanced in every version)
The constexpr keyword marks functions and variables that can be evaluated at compile time rather than runtime. When you mark a function as constexpr, the compiler can execute it during compilation and bake the result directly into your binary as a constant. This means zero runtime cost—the calculation happens once when you build your program, not every time it runs. Introduced in C++11, constexpr has been progressively enhanced in C++14, C++17, C++20, C++23, and C++26, with each version allowing more language features in compile-time contexts.
Why constexpr instead of #define?
While #define also creates compile-time constants, constexpr is vastly superior for several reasons:
- Type safety:
constexpris type-checked by the compiler;#defineis blind text substitution that can cause subtle bugs - Scope and namespaces:
constexprrespects C++ scope rules and namespaces;#defineis global and can pollute your entire codebase - Debugging:
constexprvariables show up in the debugger with their names and types;#definemacros are invisible since they're replaced before compilation - Functions:
constexprcan be actual functions with logic, loops, and conditionals;#definefunctions are just text macros prone to precedence errors - No side effects:
constexprevaluates arguments once;#definecan evaluate arguments multiple times causing unexpected behavior with expressions likeMAX(x++, y++)
#define BAD_MAX(a, b) ((a) > (b) ? (a) : (b))
constexpr auto GOOD_MAX(auto a, auto b) { return a > b ? a : b; }
int x = 5;
auto result = BAD_MAX(x++, 10); // x incremented twice! Undefined behavior
auto safe = GOOD_MAX(x++, 10); // x incremented once, as expected
C++26 expands constexpr capabilities significantly, allowing dynamic memory allocation, virtual functions, and more standard library algorithms to work in constexpr contexts.
Compile-time factorial computation:
This example shows recursive constexpr functions that execute entirely at compile time. The recursion doesn't compile indefinitely because the compiler evaluates it with a specific constant input (10 in this case) and follows the recursion until hitting the base case n <= 1. The compiler essentially "runs" the function during compilation: compute_factorial(10) calls compute_factorial(9), which calls compute_factorial(8), and so on until reaching compute_factorial(1) which returns 1. The entire chain is evaluated at compile time, and the final result (3,628,800) is baked into your binary as a constant. Compilers do have recursion depth limits to catch actual infinite recursion, but finite recursion with constant inputs works perfectly.
constexpr auto compute_factorial(int n) {
return n <= 1 ? 1 : n * compute_factorial(n - 1);
}
constexpr auto FACT_10 = compute_factorial(10);
Compile-time string parsing:
String processing can happen at compile time, turning configuration strings into values before your program even starts.
constexpr auto parse_number(std::string_view str) {
int result = 0;
for (char c : str) {
if (c >= '0' && c <= '9') {
result = result * 10 + (c - '0');
}
}
return result;
}
constexpr auto VALUE = parse_number("12345");
Ranges and forEach:
The ranges library provides functional-style iteration with lambdas, similar to JavaScript's array.forEach().
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::ranges::for_each(numbers, [](int& n) {
n *= 2;
});
Real-world applications: compile-time configuration parsing, zero-runtime-cost initialization, template metaprogramming simplification, and performance-critical calculations moved to compile time.
Pattern Matching with std::variant and std::visit (C++17)
Modern C++ provides type-safe pattern matching using std::variant and std::visit for handling multiple types. These features were introduced in C++17.
Type-safe variant matching:
A variant can hold one of several types. Using std::visit with compile-time type checking ensures you handle all possible types exhaustively.
struct Error { int code; std::string message; };
std::variant<int, std::string, Error> result = parse_input(data);
auto message = std::visit([](auto&& value) -> std::string {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, int>) {
return std::format("Got number: {}", value);
} else if constexpr (std::is_same_v<T, std::string>) {
return std::format("Got text: {}", value);
} else if constexpr (std::is_same_v<T, Error>) {
if (value.code == 404) {
return "Not found";
}
return std::format("Error code: {}", value.code);
}
}, result);
This approach is conceptually similar to Rust's match statement, which also requires exhaustive pattern matching—all possible cases must be handled at compile time. Both C++'s std::visit and Rust's match provide the same fundamental guarantee: the compiler ensures you haven't forgotten to handle any variant types, catching errors at compile time rather than discovering them at runtime.
Real-world applications for variant/visit: parsing network protocols, handling API responses, state machine implementations, compiler/interpreter AST (Abstract Syntax Tree) processing, and type-safe error handling.
Safe Optional Access (C++17)
If-initializers (C++17) combined with std::optional (C++17) provide clean, safe access to potentially missing values without nested conditionals. This is separate from variant matching but equally important for type safety.
std::optional<User> find_user(int id);
if (auto user = find_user(id); user && user->admin) {
log(std::format("Admin {} accessed system", user->name));
}
The if-initializer declares user in the condition itself, automatically checking if the optional contains a value. When user is tested in the boolean context, it returns true if the optional holds a value, false if empty. If find_user returns an empty optional, the condition fails and the block is skipped—no null pointer dereferences possible.
Standard Library Improvements
C++26 enhances containers, algorithms, and ranges with better performance and ergonomics.
String formatting (C++20)
Modern format strings provide type-safe, printf-style formatting with support for custom types and chrono formatting.
auto message = std::format("User {} logged in at {:%Y-%m-%d %H:%M}",
user.name, std::chrono::system_clock::now());
Ranges and views (C++20, enhanced in C++23/26)
Ranges and views enable functional-style composition of operations on containers without creating intermediate copies. A view is a lazy, non-owning range that applies transformations only when you actually iterate through the data. This approach is both more expressive and more efficient than traditional loops or algorithms. The ranges library was introduced in C++20, with std::ranges::to added in C++23.
auto valid_users = users
| std::views::filter([](auto& u) { return u.active; })
| std::views::transform([](auto& u) { return u.email; })
| std::ranges::to<std::vector>();
This pipeline filters for active users, extracts their email addresses, and collects them into a vector—all in a single pass with no temporary vectors created between steps. The pipe operator | makes the data flow visually clear, reading left-to-right like a Unix pipeline. Compare this to the traditional approach:
std::vector<std::string> valid_users;
for (const auto& u : users) {
if (u.active) {
valid_users.push_back(u.email);
}
}
The ranges version is more declarative (what you want) rather than imperative (how to do it), and the compiler can optimize the entire pipeline as a single operation. Views are lazy: if you never iterate through the result, no work is done. You can chain as many view operations as you want with zero performance penalty until you materialize the result with std::ranges::to or iterate through it.
If you're familiar with C# LINQ, this is essentially the same concept with nearly identical semantics: lazy evaluation, functional composition, and deferred execution until materialization. The main difference is syntax—C# uses method chaining while C++ uses the pipe operator.
Flat containers (C++23)
Flat containers store their elements in contiguous memory (like a sorted vector) instead of using tree structures with pointer indirection. Traditional std::map uses a red-black tree where each node is allocated separately—traversing the tree means following pointers scattered throughout memory, causing cache misses. Flat containers keep everything in a single contiguous array, dramatically improving cache locality and iteration speed.
std::flat_map<std::string, int> config;
config["timeout"] = 30;
For small-to-medium sized datasets (typically under a few thousand elements), std::flat_map outperforms std::map for most operations. Lookups use binary search on a sorted array, which is cache-friendly even though it's O(log n) like tree traversal. Insertion and deletion are slower (O(n) due to shifting elements) compared to tree-based containers, but the cache efficiency often makes up for it in practice. Use flat containers when you have relatively static data, need fast iteration, or when memory usage matters—they use significantly less memory than tree-based structures since they don't need node pointers.
Linear algebra (C++26 proposal)
Built-in matrix and vector operations for scientific computing and graphics.
std::linalg::matrix<double, 3, 3> rotation = {
{1, 0, 0},
{0, cos(θ), -sin(θ)},
{0, sin(θ), cos(θ)}
};
auto transformed = rotation * point;
Hardware acceleration and optimization
std::linalg is primarily designed for CPU-based matrix operations with potential SIMD vectorization (SSE, AVX, AVX-512) through compiler auto-vectorization. However, the relationship with specialized hardware accelerators is more nuanced:
Intel Advanced Matrix Extensions (AMX): AMX provides hardware-accelerated tile-based matrix multiply operations (TMUL) on newer Xeon processors. While compilers can theoretically emit AMX instructions for std::linalg operations, in practice this is unlikely without explicit intrinsics. AMX requires specific data layouts (tiles) and explicit programming via intrinsics or libraries like oneMKL. The standard library abstraction won't automatically leverage AMX—you'd need to use Intel's optimized BLAS implementations (MKL) or write explicit AMX code for critical sections.
The explicit AMX intrinsics include (from <immintrin.h>):
_tile_loadconfig(config)- Configure tile registers_tile_loadd(tile, base, stride)- Load data into tile_tile_stored(tile, base, stride)- Store tile to memory_tile_dpbssd(dst, src1, src2)- INT8 matrix multiply-accumulate_tile_dpbf16ps(dst, src1, src2)- BF16 matrix multiply-accumulate_tile_release()- Release tile configuration
These intrinsics operate on 8 tile registers (TMM0-TMM7), each capable of holding up to 1KB of data in configurable row×column layouts. Using AMX requires manual tile configuration, explicit data layout management, and careful orchestration of loads, multiplies, and stores—far removed from the high-level matrix * vector abstraction that std::linalg provides. Think of std::linalg as providing portable, moderately-optimized CPU operations, not cutting-edge hardware acceleration.
Practical approach
std::linalg provides a standard, portable CPU interface for small-to-medium matrix operations with compiler SIMD optimizations. For large-scale performance-critical work, use specialized libraries: Intel MKL (with AMX) for CPU, cuBLAS/CUTLASS for NVIDIA GPUs.
Real-world applications: high-performance data processing pipelines, configuration management, game engines (matrices and vectors), financial calculations, and data transformation workflows.
Enhanced Concurrency (C++20/26)
C++26 improves thread safety, async programming, and parallel execution. Coroutines were introduced in C++20, with further concurrency improvements in C++26.
Structured concurrency
Parallel execution with automatic lifetime management ensures resources are properly cleaned up.
std::vector<std::string> urls = get_urls();
std::vector<Response> responses;
std::execution::run(
std::execution::on(thread_pool),
[&] {
std::ranges::transform(urls, std::back_inserter(responses),
[](const auto& url) { return fetch(url); },
std::execution::par);
}
);
Atomic operations (std::atomic_ref - C++20)
Atomic references allow atomic operations on non-atomic variables without changing their declaration.
std::atomic<int> counter = 0;
std::atomic_ref<int> ref(counter);
Coroutines for async I/O (C++20)
Co_await syntax enables asynchronous I/O without callback hell, keeping code linear and readable.
task<Response> fetch_data(std::string url) {
auto connection = co_await connect(url);
auto response = co_await connection.read();
co_return response;
}
Concurrent data structures
Thread-safe containers eliminate the need for manual locking in many scenarios.
std::concurrent_queue<Task> work_queue;
Real-world applications: web servers handling concurrent requests, parallel data processing, real-time systems, high-frequency trading, game engines with job systems, and any application requiring thread-safe communication between components.
Smart Pointers: Modern Memory Management (C++11)
Smart pointers are the foundation of safe memory management in C++. They provide automatic memory management through RAII (Resource Acquisition Is Initialization), eliminating most manual new/delete operations and preventing memory leaks. Smart pointers were introduced in C++11 and are now the standard way to manage heap-allocated objects.
std::unique_ptr - Exclusive Ownership (C++11)
std::unique_ptr is the default smart pointer you should reach for in modern C++. It represents exclusive ownership of a dynamically allocated object, meaning there's always exactly one owner responsible for the resource. When the unique_ptr goes out of scope—whether through normal function return, exception unwinding, or being reassigned—it automatically deletes the object it owns, making memory leaks virtually impossible. The key insight is that unique_ptr has zero runtime overhead compared to a raw pointer: it's the same size, accesses are just as fast, and in optimized builds, it compiles down to exactly the same machine code you'd write with manual new/delete. The difference is that the compiler enforces ownership semantics at compile time, preventing you from accidentally copying the pointer (which would create two owners of the same resource) while still allowing you to transfer ownership explicitly with move semantics.
// Automatic cleanup when out of scope
auto ptr = std::make_unique<MyClass>(arg1, arg2);
ptr->method();
// Transfer ownership with move semantics
auto other = std::move(ptr); // ptr is now nullptr
// Use in containers
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());
Use std::make_unique (C++14) instead of new for two reasons: exception safety (if the constructor throws, make_unique ensures no leak) and cleaner syntax. Since unique_ptr is move-only, you can store them in containers and transfer ownership between functions without any runtime cost, making it perfect for implementing ownership hierarchies, factory functions, and RAII wrappers around any resource (not just memory—file handles, network connections, etc.). The move-only semantics aren't a limitation; they're a feature that makes ownership explicit in your code's type system.
std::shared_ptr - Shared Ownership (C++11)
std::shared_ptr implements shared ownership through automatic reference counting, allowing multiple pointers to co-own the same object. Each shared_ptr maintains a control block that tracks how many shared_ptrs point to the resource, and when the last shared_ptr is destroyed or reassigned, the object is automatically deleted. This is particularly useful when you have complex object graphs where ownership isn't hierarchical—for example, when multiple data structures need to reference the same cache entries, or when implementing observer patterns where multiple observers need to safely reference a subject that might be destroyed at any time. The reference counting is thread-safe (incrementing and decrementing the count uses atomic operations), so you can safely copy shared_ptrs across threads, though the pointed-to object itself isn't automatically thread-safe unless you add synchronization.
// Multiple owners, automatic cleanup when last reference dies
auto ptr1 = std::make_shared<Resource>();
auto ptr2 = ptr1; // Reference count = 2
auto ptr3 = ptr1; // Reference count = 3
// Resource deleted when all shared_ptrs are destroyed
The cost of shared_ptr is higher than unique_ptr: it requires a separate control block allocation (though make_shared optimizes this into a single allocation), and each copy/move operation must atomically modify the reference count. However, this overhead is typically negligible compared to the complexity and bugs you'd introduce trying to manually track ownership. Use shared_ptr when you genuinely need shared ownership—don't default to it just because it's more flexible. The single-owner discipline enforced by unique_ptr leads to clearer code and better performance in most cases. When you do need shared ownership, always use make_shared rather than new, as it's more efficient and exception-safe.
std::weak_ptr - Breaking Cycles
std::weak_ptr is the solution to shared_ptr's one major weakness: circular references. When two objects hold shared_ptrs to each other (like a parent-child relationship where both need references to the other), neither reference count ever reaches zero, creating a memory leak. A weak_ptr acts as a non-owning observer that can see a shared_ptr's object without incrementing its reference count. This means the object can be deleted while weak_ptrs still reference it, so before accessing the object, you must "promote" the weak_ptr to a shared_ptr using the lock() method, which atomically checks if the object still exists and creates a temporary owning reference if it does. This makes weak_ptr perfect for implementing caches (where entries can be evicted), observer patterns (where observers don't keep subjects alive), and parent-child relationships (where children shouldn't keep parents alive).
std::shared_ptr<Node> parent = std::make_shared<Node>();
std::weak_ptr<Node> child_ref = parent;
// Check if resource still exists
if (auto ptr = child_ref.lock()) {
ptr->method(); // Safe to use
} else {
// Resource was deleted
}
The typical usage pattern is to store a weak_ptr as a member variable and call lock() every time you need to access the object. The returned shared_ptr (if the object still exists) keeps the object alive for the duration of your operation, preventing it from being deleted while you're using it. This is much safer than storing a raw pointer, which would become a dangling pointer if the object was deleted, giving you no way to detect the problem. weak_ptr adds minimal overhead—just one pointer to the control block—and makes it possible to build complex object graphs with shared ownership while avoiding the circular reference leaks that would otherwise be inevitable.
Safe View Types
C++ provides several safe view types that reference data without ownership:
std::string_view - Non-Owning String Reference (C++17)
std::string_view is a lightweight, non-owning reference to a string stored elsewhere in memory. Think of it as a pointer to a character array combined with a length, without the overhead of copying or allocating memory. When you pass a string to a function that only needs to read it, using string_view means you're just passing a pointer and size (two machine words), regardless of how long the string actually is. This makes it incredibly efficient for function parameters, especially when you want to accept both std::string objects, C-style strings, and string literals with a single function signature. The view doesn't manage the memory—it just looks at it—so you must ensure the underlying string data outlives the string_view that references it.
void process(std::string_view text) {
// Works with std::string, const char*, and string literals
// No copying, no allocation
std::cout << text << '\n';
}
std::string str = "Hello";
process(str); // No copy
process("World"); // No copy
The key advantage is zero-copy performance combined with flexibility. You can create substrings using operations like .substr() without allocating new memory—it just creates a new view into the same underlying data. However, this non-ownership comes with responsibility: if the original string is destroyed or moved while a string_view is still referencing it, you'll have a dangling reference leading to undefined behavior.
Warning: string_view doesn't own its data. The underlying string must outlive the view.
std::span - Array View (C++20)
std::span is a non-owning view over a contiguous sequence of objects, essentially providing a safe and modern interface to arrays. Unlike raw pointers, a span knows its own size, making it impossible to accidentally access memory beyond the array's bounds when using its safe accessors. It solves one of C++'s oldest problems: passing arrays to functions without losing size information or requiring templates for every different container type. A span is just a pointer to the first element plus a count—two machine words—so passing it to a function costs the same as passing a pointer, but you get bounds information for free. The beauty of span is its universality: it works seamlessly with std::vector, std::array, C-style arrays, and any other contiguous container, allowing you to write one function that accepts them all without any copying or template metaprogramming gymnastics.
void process_array(std::span<const int> values) {
// Works with std::vector, std::array, C arrays
for (int value : values) {
std::cout << value << '\n';
}
}
std::vector<int> vec = {1, 2, 3, 4};
std::array<int, 3> arr = {5, 6, 7};
int raw[] = {8, 9, 10};
process_array(vec); // All work without copying
process_array(arr);
process_array(raw);
Like string_view, span is non-owning, meaning the underlying array must remain valid for the span's lifetime. But unlike raw pointers or iterator pairs, span provides methods like .at() for bounds-checked access and .subspan() for creating views of subranges. This makes span the modern replacement for the old C idiom of passing a pointer and size as separate parameters.
std::mdspan - Multidimensional Array View (C++23)
std::mdspan extends the span concept to multidimensional arrays, providing a non-owning view over matrices, tensors, and other multidimensional data structures. While span handles one-dimensional contiguous arrays, mdspan understands the layout and dimensions of multidimensional data, letting you index it naturally with matrix[i, j] syntax instead of manually computing flat indices like matrix[i * width + j]. It's particularly powerful because it separates the data storage from its interpretation: the same flat array of memory can be viewed as a 3x4 matrix, a 2x2x3 tensor, or with different memory layouts (row-major for C-style, column-major for Fortran-style, or custom strided layouts). This makes mdspan essential for scientific computing, image processing, and any domain working with multidimensional data, as you can pass arrays between different subsystems without copying or reformatting—just reinterpret the view.
void process_matrix(std::mdspan<int, std::extents<size_t, 3, 4>> matrix) {
// Safe multidimensional array access
for (size_t i = 0; i < matrix.extent(0); ++i) {
for (size_t j = 0; j < matrix.extent(1); ++j) {
matrix[i, j] *= 2;
}
}
}
The real power of mdspan comes from its layout customization. You can specify row-major, column-major, or strided layouts, and even create subviews (slices) of multidimensional arrays without copying data. This makes it possible to interface C++ code with Fortran libraries, GPU arrays, or memory-mapped files while maintaining natural indexing syntax and zero-copy performance.
Modern Container Safety
std::optional - Nullable Values (C++17)
std::optional is C++'s type-safe solution to representing values that may or may not exist, effectively replacing the error-prone practice of using null pointers or special sentinel values like -1 to indicate "no value." It wraps a value of any type and explicitly tracks whether that value is present, forcing you to check before accessing it. This eliminates an entire class of bugs where you might accidentally dereference a null pointer or forget to check for a sentinel value. When a function might fail to produce a result—like searching for an element in a container, parsing a string, or performing a lookup—returning std::optional makes the possibility of failure explicit in the type system. The caller must consciously handle both the success and failure cases, typically with an if statement that checks whether the optional contains a value before using it.
std::optional<int> find_value(const std::vector<int>& vec, int target) {
auto it = std::find(vec.begin(), vec.end(), target);
if (it != vec.end()) {
return *it;
}
return std::nullopt;
}
// Safe usage
if (auto result = find_value(numbers, 42)) {
std::cout << "Found: " << *result << '\n';
} else {
std::cout << "Not found\n";
}
The beauty of optional is that it makes your intent clear and your code safer without runtime overhead. Unlike nullable references in garbage-collected languages, optional has zero space overhead for small types (it's typically just the value plus a bool), and accessing the value is as fast as accessing a regular variable once you've checked that it exists. It's particularly useful in APIs where returning a reference or pointer might be ambiguous or dangerous, and it pairs perfectly with modern C++ features like structured bindings and if-initializers.
std::variant - Type-Safe Unions (C++17)
std::variant provides a type-safe alternative to C unions, allowing you to store one value from a set of possible types while always knowing exactly which type is currently stored. Unlike C unions where accessing the wrong member invokes undefined behavior, variant tracks which type it currently holds and prevents you from accessing it incorrectly. This makes it perfect for representing values that could be one of several distinct types—like an error code or a success value, a result that could be an integer or a floating-point number, or any situation where you need sum types (also called tagged unions or discriminated unions in other languages). The type information is maintained at runtime, so you can safely query which type is active and extract it without fear of memory corruption or undefined behavior.
std::variant<int, double, std::string> value;
value = 42; // Holds int
value = 3.14; // Now holds double
value = "Hello"; // Now holds string
// Safe access with std::visit
std::visit([](auto&& v) {
std::cout << v << '\n';
}, value);
The real power of variant comes from std::visit, which lets you write exhaustive handlers that cover all possible types in the variant. The compiler will verify that you've handled every case, making it impossible to forget a type and helping you write robust code that doesn't break when you add new types to a variant. This makes variant ideal for implementing state machines, parsers, AST nodes, or any data structure where different nodes might have fundamentally different types but need to be stored in the same container. Unlike using inheritance and virtual functions, variant has no heap allocation, no vtable overhead, and perfect cache locality.
Why Smart Pointers Matter
Memory safety issues account for ~70% of security vulnerabilities. Smart pointers eliminate entire classes of bugs:
Prevented bugs:
- Memory leaks (forgot to delete)
- Double free (deleted twice)
- Use after free (dangling pointers)
- Null pointer dereferences (with proper checking)
- Resource leaks (file handles, sockets, etc.)
Best practices:
- Prefer std::make_unique and std::make_shared - safer and more efficient
- Use unique_ptr by default - switch to shared_ptr only when needed
- Avoid raw
newanddelete- let smart pointers manage memory - Use weak_ptr to break cycles - prevents circular reference leaks
- Return smart pointers from factories - clear ownership transfer
The Future: Safe C++
For those evaluating C++ versus Rust for memory safety, the C++ community is actively developing Safe C++, a proposed extension that brings Rust-style borrow checking and rigorous memory safety to C++. While still in development, Safe C++ represents a potential path forward for memory-safe systems programming in C++ without requiring a complete language migration.
Key insight: C++ is evolving toward stronger safety guarantees. Using modern C++ features (smart pointers, spans, string_view, optional, variant) positions your code for future safety improvements and demonstrates current best practices.
Compiler Configuration
Compiler Preferences
- Prefer clang++ over g++ when available
- Use Werror - treat all warnings as errors
- Enable compiler hardening options for security and robustness
CMake Compiler Configuration
# Prefer clang++ if available
if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "Using Clang compiler")
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(STATUS "Using GCC compiler")
endif()
# Enable warnings as errors
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror")
# Compiler hardening options
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wpedantic")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wformat=2 -Wformat-security")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wstack-protector")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fstack-protector")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -fPIE")
# Linker hardening options
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-z,relro -Wl,-z,now")
Hardening Options Explained
-Werror- Treat all warnings as errors (fail build on warnings)-Wall -Wextra -Wpedantic- Enable comprehensive warning sets-Wformat=2 -Wformat-security- Warn about format string vulnerabilities-Wstack-protector- Enable stack protection warnings-fstack-protector- Enable stack protection-D_FORTIFY_SOURCE=2- Enable buffer overflow detection-fPIC -fPIE- Position independent code/executables-Wl,-z,relro -Wl,-z,now- Linker hardening (relocation read-only, immediate binding)
Handling Compiler Warnings
Do not override warnings in CMake. Use pragma directives for specific cases:
// For external headers that fail warnings (-Werror)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-parameter"
#include <external/library.hpp>
#pragma clang diagnostic pop
// For unused parameters
void myFunction(int used_param, [[maybe_unused]] int unused_param) {
// Function body
}
// For unused variables
[[maybe_unused]] int debug_variable = 42;
// Alternative for parameters
void anotherFunction(int used_param, int unused_param) {
(void)unused_param; // Silence unused parameter warning
}
Project Structure Overview
Basic Directory Layout
projectname/
├── src/ # Source files (.cpp)
├── include/ # Header files (.hpp)
├── test/ # Test files (mirrors src/ structure)
├── cmake/ # CMake modules and plugins
├── CMakeLists.txt # Build configuration
├── .clang-format # Code formatting rules
└── .gitignore # Version control exclusions
Single Project Example
projectname/
├── src/
│ ├── main.cpp
│ ├── logger.cpp # Standalone: namespace projectname::logger
│ ├── parser.cpp # Standalone: namespace projectname::parser
│ ├── interface/ # Concept with implementations
│ │ ├── lib.cpp
│ │ ├── ethernet.cpp # namespace projectname::interface
│ │ ├── tunnel.cpp # namespace projectname::interface
│ │ └── bridge.cpp # namespace projectname::interface
│ └── window/ # Compound word split
│ ├── lib.cpp
│ └── main.cpp # MainWindow → window/main
├── include/
│ ├── logger.hpp
│ ├── parser.hpp
│ ├── interface/
│ │ ├── lib.hpp
│ │ ├── ethernet.hpp
│ │ ├── tunnel.hpp
│ │ └── bridge.hpp
│ └── window/
│ ├── lib.hpp
│ └── main.hpp
└── test/ # Mirrors src/ structure
├── Kyuafile
├── logger.cpp
├── parser.cpp
├── interface/
│ ├── ethernet.cpp
│ └── tunnel.cpp
└── window/
└── main.cpp
Workspace with Multiple Projects
workspace/
├── project1/
│ ├── src/
│ │ ├── main.cpp # namespace workspace::project1
│ │ └── logger.cpp # namespace workspace::project1::logger
│ ├── include/
│ │ └── logger.hpp
│ └── CMakeLists.txt
├── project2/
│ ├── src/
│ │ ├── main.cpp # namespace workspace::project2
│ │ └── handler/
│ │ └── request.cpp # namespace workspace::project2::handler
│ ├── include/
│ │ └── handler/
│ │ └── request.hpp
│ └── CMakeLists.txt
└── CMakeLists.txt # Root workspace makefile
File Organization Rules
One Class Per File
- Each class gets its own
.cppand.hppfiles - Use single-word filenames (no underscores, dashes, spaces)
- Use subdirectories for grouping related classes
When to Use Subdirectories
Use subdirectories for:
- Concepts with multiple implementations (e.g.,
interface/withethernet,tunnel,bridge) - Compound word class names (e.g.,
MainWindow→window/main.hpp) - Related classes that share a common purpose (e.g.,
component/controller,component/service)
Don't use subdirectories for:
- Standalone components (e.g.,
logger,parser,validator) - Single classes with unique single-word names
Subdirectory Requirements
- Each subdirectory must have
lib.hppandlib.cpp(even if empty) - All files in a subdirectory share the same namespace
- Namespace pattern:
namespace projectname::subdirectory
File Naming Examples
- ✅
logger.hpp(standalone) - ✅
interface/ethernet.hpp(concept with multiple implementations) - ✅
window/main.hpp(compound word "mainwindow") - ❌
ethernet_interface.hpp(useinterface/ethernet.hpp) - ❌
mainwindow.hpp(usewindow/main.hpp)
Concepts vs Implementations
Concept (needs subdirectory):
- "interface" has multiple types: ethernet, tunnel, bridge, lagg, tap
- Each type is distinct but related
- Directory:
interface/with files:ethernet.cpp,tunnel.cpp, etc.
Standalone (no subdirectory):
- "logger" is a single, self-contained component
- No multiple types or variations
- Files:
logger.cppandlogger.hppat root level
Class Name to Directory Mapping
Remove base class prefixes when creating directories:
BaseObject→object/directoryBaseWidget→widget/directoryBaseDialog→dialog/directoryMainWindow→window/main.hppPushButton→button/push.hppFileOpenDialog→dialog/file/open.hpp
Example:
include/
├── object/ # BaseObject-derived classes
│ ├── lib.hpp
│ ├── timer.hpp # namespace projectname::object
│ └── thread.hpp # namespace projectname::object
├── widget/ # BaseWidget-derived classes
│ ├── lib.hpp
│ ├── button.hpp # namespace projectname::widget
│ ├── label.hpp # namespace projectname::widget
│ └── input.hpp # namespace projectname::widget
├── window/ # BaseWindow-derived classes
│ ├── lib.hpp
│ └── main.hpp # MainWindow → namespace projectname::window
└── dialog/ # BaseDialog-derived classes
├── lib.hpp
├── file/ # File dialogs
│ ├── lib.hpp
│ ├── open.hpp # FileOpenDialog → namespace projectname::dialog::file
│ └── save.hpp # FileSaveDialog → namespace projectname::dialog::file
├── message/ # Message dialogs
│ ├── lib.hpp
│ ├── info.hpp # namespace projectname::dialog::message
│ └── error.hpp # namespace projectname::dialog::message
└── input.hpp # InputDialog → namespace projectname::dialog
Namespace Conventions
Core Rules
CRITICAL: Root namespace must match the project directory name exactly.
Use compact syntax:
// ✅ Correct
namespace projectname::component {
// code
}
// ❌ Incorrect - nested blocks
namespace projectname {
namespace component {
// code
}
}
Single Project Namespaces
// File: src/logger.cpp
namespace projectname::logger {
class Logger { };
}
// File: src/interface/ethernet.cpp
namespace projectname::interface {
class Ethernet { };
}
// File: src/interface/tunnel.cpp
namespace projectname::interface {
class Tunnel { };
}
Workspace Namespaces
// File: workspace/project1/src/logger.cpp
namespace workspace::project1::logger {
class Logger { };
}
// File: workspace/project2/src/handler/request.cpp
namespace workspace::project2::handler {
class Request { };
}
Namespace to Directory Mapping
Key principle: Namespace follows directory structure, but NOT src/ or include/.
✅ Correct:
// File: src/logger.cpp
namespace projectname::logger { }
// File: src/interface/ethernet.cpp
namespace projectname::interface { }
// File: src/component/controller.cpp
namespace projectname::component { }
❌ Incorrect:
// ❌ Generic root namespace
namespace app::component { }
// ❌ Including src/ or include/
namespace projectname::src::component { }
namespace projectname::include::module { }
Shared Namespaces
All files in a subdirectory share the same namespace:
// interface/ethernet.hpp
namespace projectname::interface {
class Ethernet { void configure(); };
}
// interface/tunnel.hpp
namespace projectname::interface {
class Tunnel { void establish(); };
}
// interface/bridge.hpp
namespace projectname::interface {
class Bridge { void addPort(); };
}
The namespace represents the concept (interface), not the implementation (ethernet, tunnel, bridge).
Naming Conventions
- Class and Struct Names:
ClassNamesLikeThis(PascalCase) - Function Names:
funcNamesLikeThis(camelCase) - Enum Names:
EnumNamesLikeThis(PascalCase) - Enum Properties:
EnumNamesLikeThis::enumPropertiesLikeThis(camelCase) - Constants:
CONSTANTS_LIKE_THIS(UPPER_SNAKE_CASE) - Local Variables:
local_variables_like_this(snake_case) - Function Parameters:
function_parameters_like_this(snake_case) - Class Properties:
_class_properties_like_this(snake_case with leading underscore)
Code Style
clang-format Configuration
Every project should include .clang-format in the root:
# .clang-format
NamespaceIndentation: All
Formatting commands:
# Format all C++ files
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
# Preview changes
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format
# Format specific directories
find src -name "*.cpp" | xargs clang-format -i
find include -name "*.hpp" | xargs clang-format -i
Header Files
- Use
#include <header.hpp>syntax (angle brackets) - All headers must have include guards
- No forward declarations when full includes are available
- Include all necessary headers directly
File Extensions
- Source files:
.cpp - Header files:
.hpp
Commenting Standards
Avoid Inline Comments
- Avoid
//comments - only use for non-self-explanatory code - Prefer self-documenting code over comments
Documentation Comments (Headers Only)
- Document all public functions and properties in header files
- Do not put documentation comments in implementation files
- Use Doxygen-style comments
Function Documentation
/**
* @brief Brief description of what the function does
* @param paramName Description of parameter
* @return Description of return value
* @throws ExceptionType When this exception is thrown
*/
void myFunction(int paramName);
Property Documentation
/**
* @brief Brief description of what the property represents
* @details Additional details if needed
*/
std::string myProperty;
Class Documentation
/**
* @brief Brief description of class purpose
* @details Detailed description of class functionality and usage
*/
class MyClass {
// class implementation
};
File Headers
Every file must have a header:
/**
* @file filename.cpp
* @brief Brief description of file purpose
* @details Detailed description if needed
*
* @author paigeadelethompson
* @year [Use: date +%Y to get current year]
*/
CMake Build System
CMake Version Requirements
cmake_minimum_required(VERSION 3.31.6)
Directory Organization
cmake/directory is for CMake plugins and modules only- Each subdirectory should have its own
CMakeLists.txt
Top-Level CMakeLists.txt
Responsibilities:
- Define ALL include paths (not in sub-makefiles)
- Handle linking (static only)
- Include subdirectory makefiles
# Define global include directory
set(GLOBAL_PROJ_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/include)
include_directories(${GLOBAL_PROJ_INCLUDE_DIR})
# For workspace with multiple projects
set(GLOBAL_PROJ_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/projectname/include)
include_directories(${GLOBAL_PROJ_INCLUDE_DIR})
Include path rules:
- Define include path as a variable
- Use
include_directories()to set global include path - NEVER define specific header file paths - only directory paths
- This enables
#include <subdir/header.hpp>throughout the project
Subdirectory CMakeLists.txt
Responsibilities:
- Only produce
.aartifacts (static libraries) - All linking must be static
- Reference the global include directory
- Link their own subdirectories
- No linking to peer directories (handled by parent)
# Include global directories
include_directories(${GLOBAL_PROJ_INCLUDE_DIR})
# Add subdirectories
add_subdirectory(service)
# Compile sources to static library
add_library(projectname_component STATIC
source1.cpp
source2.cpp
)
# Link subdirectory libraries
target_link_libraries(projectname_component projectname_service)
CMake Target Naming
Targets should follow namespace structure:
Single project:
add_library(projectname_component STATIC controller.cpp service.cpp)
add_library(projectname_window STATIC main.cpp)
add_library(projectname_module STATIC processor.cpp handler.cpp)
Multiple projects:
add_library(workspace_project1_component STATIC controller.cpp)
add_library(workspace_project1_window STATIC main.cpp)
add_library(workspace_project2_handler STATIC request.cpp)
Package Management
Discovery priority:
find_package()- Use CMake's built-in package discovery firstpkg-config- Use as fallback when find_package fails- Manual search - Only use find_library/find_path as last resort
# Set up pkg-config
find_package(PkgConfig REQUIRED)
set(ENV{PKG_CONFIG_PATH} "/usr/lib/pkgconfig/:/usr/local/lib/pkgconfig/")
# Preferred: find_package
find_package(OpenSSL REQUIRED)
# Fallback: pkg-config
pkg_check_modules(LIBYANG REQUIRED libyang)
# Last resort: manual
find_library(PTHREAD_LIBRARY pthread)
qmake Build System (Qt Projects)
Why qmake for Qt Projects
qmake is purpose-built for Qt and handles everything automatically. It's the official Qt build system and requires minimal configuration.
Automatic Code Generation
qmake automatically handles all Qt-specific code generation with zero configuration:
1. MOC (Meta-Object Compiler)
- Automatically detects
Q_OBJECTmacros and runs MOC - Generates code for signals, slots, properties, and Qt's reflection system
- Works correctly out of the box with no setup required
2. UIC (User Interface Compiler)
- Automatically compiles
.uifiles toui_*.hheaders - Converts Qt Designer UI files to C++ code
- Just add
.uifile toFORMSvariable, everything else is automatic
3. RCC (Qt Resource Compiler)
- Automatically compiles
.qrcfiles toqrc_*.cpp - Embeds images, fonts, translations, and other resources into your executable
- Just add
.qrcfile toRESOURCESvariable
4. Translation Files
- Built-in support with
TRANSLATIONSvariable - Automatically generates and embeds
.tstranslation files - One line:
TRANSLATIONS += app_en.ts app_es.ts
Simple Module Management
Qt modules are simple and concise:
QT += core gui widgets network sql multimedia
This automatically:
- Links all necessary Qt libraries
- Sets up include paths
- Configures compiler flags
- Handles transitive dependencies between Qt modules
Built-in Platform Detection
Platform-specific code is straightforward:
unix:!macx {
# Linux-specific configuration
}
macx {
# macOS-specific configuration
}
win32 {
# Windows-specific configuration
}
Integrated Deployment Tools
qmake integrates seamlessly with Qt's deployment tools:
windeployqtfor Windowsmacdeployqtfor macOSlinuxdeployqtfor Linux
These tools automatically bundle all Qt dependencies with your application.
Qt Plugin Support
Qt plugins work automatically:
QT += pluginfor plugin development- Static plugins are initialized automatically
- Dynamic plugins are discovered and loaded correctly
Qt-Specific Features
qmake automatically handles:
- Qt version detection - knows which Qt installation you're using
- Qt installation paths - finds Qt libraries and tools automatically
- Qt build flags - sets correct compiler flags for Qt features
- Qt module dependencies - handles transitive dependencies
- Qt private headers - correctly includes private Qt headers when needed
- Qt frameworks - handles macOS frameworks correctly
Real-World Example
Complete Qt project in 9 lines:
QT += core gui widgets network
TARGET = myapp
TEMPLATE = app
SOURCES += main.cpp mainwindow.cpp
HEADERS += mainwindow.h
FORMS += mainwindow.ui
RESOURCES += resources.qrc
TRANSLATIONS += app_en.ts
This handles all MOC, UIC, RCC processing, module linking, and deployment configuration automatically.
When to Use qmake
- Always use qmake for Qt projects - it's designed for Qt
- Use Qt6 for new projects
- Avoid CMake for Qt unless you have non-Qt dependencies that absolutely require CMake
- Save time and avoid headaches - qmake just works
Prerequisites
- Qt6 development tools installed
qmake6in PATH- C++26 compatible compiler
Build Process
# Generate Makefile
qmake6 projectname.pro
# Build with parallel compilation
make -j $(nproc)
# Run the application
./projectname
Build Commands
# Clean build
make clean
# Rebuild from scratch
make clean && make -j $(nproc)
# Debug build
qmake6 CONFIG+=debug projectname.pro
make -j $(nproc)
# Release build (default)
qmake6 CONFIG+=release projectname.pro
make -j $(nproc)
Parallel Build Optimization
- Always use
make -j $(nproc)for optimal performance $(nproc)automatically detects CPU cores- Standard practice for make-based builds
Testing with ATF and Kyua
Installation
Debian/Ubuntu:
sudo apt install libatf-c++-2 kyua
FreeBSD:
sudo pkg install atf kyua
Arch Linux:
sudo pacman -S atf kyua
macOS:
brew install atf kyua
Test Directory Structure
Tests mirror the src/ structure:
project/
├── src/
│ ├── logger.cpp
│ ├── parser.cpp
│ └── interface/
│ ├── ethernet.cpp
│ └── tunnel.cpp
└── test/ # Mirrors src/
├── Kyuafile
├── logger.cpp # Tests src/logger.cpp
├── parser.cpp # Tests src/parser.cpp
└── interface/ # Mirrors src/interface/
├── ethernet.cpp # Tests src/interface/ethernet.cpp
└── tunnel.cpp # Tests src/interface/tunnel.cpp
ATF Test Program Structure
#include <atf-c++.hpp>
ATF_TEST_CASE(test_functionality);
ATF_TEST_CASE_BODY(test_functionality) {
ATF_REQUIRE(condition);
ATF_REQUIRE_EQ(expected, actual);
ATF_PASS();
}
ATF_INIT_TEST_CASES(tcs) {
ATF_ADD_TEST_CASE(tcs, test_functionality);
}
Kyuafile Configuration
Kyuafiles are Lua scripts:
-- Kyuafile
syntax(2)
-- Define test suite
test_suite('projectname')
-- Register test programs
atf_test_program{name="test_program"}
atf_test_program{name="logger"}
atf_test_program{name="parser"}
-- Include subdirectories
include('interface/Kyuafile')
Kyuafile Metadata
atf_test_program{name="test_program",
allowed_architectures='amd64 i386',
required_files='/bin/ls',
timeout=30,
required_user='root'}
Common properties:
allowed_architectures- Whitespace-separated architecturesrequired_files- Space-separated file pathsrequired_user- 'root' or 'unprivileged'timeout- Timeout in secondsexecenv- Execution environment (platform-specific)
CMake Test Integration
# Create test executable
add_executable(test_program test_program.cpp)
# Link ATF library
find_package(PkgConfig REQUIRED)
pkg_check_modules(ATF REQUIRED atf-c++)
target_link_libraries(test_program ${ATF_LIBRARIES})
target_include_directories(test_program PRIVATE ${ATF_INCLUDE_DIRS})
# Set test output to build root
set_target_properties(test_program PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}
)
# Copy Kyuafile to build directory
configure_file(
${CMAKE_SOURCE_DIR}/tests/Kyuafile
${CMAKE_BINARY_DIR}/Kyuafile
COPYONLY
)
# Create test target (not default)
add_custom_target(run_tests
COMMAND kyua test
DEPENDS test_program
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Running ATF tests with Kyua"
)
Running Tests
# Build and run tests
make run_tests
# Run tests manually
cd build/
kyua test
# Run specific test
kyua test test_program:test_functionality
# View results
kyua report --verbose
ATF Macros
ATF_REQUIRE(condition)- Fail if falseATF_REQUIRE_EQ(expected, actual)- Require equalityATF_CHECK(condition)- Check but continueATF_PASS()- Mark as passedATF_FAIL(reason)- Mark as failedATF_SKIP(reason)- Skip test
Manual Pages
man kyuafile # Kyuafile syntax
man kyua # Test execution
man atf-c++ # ATF C++ API
Best Practices
Error Handling
Special case: All errors in single files:
src/exception.cpp
include/exception.hpp
Example:
// include/exception.hpp
namespace projectname {
class ProjectNameBaseError : public std::exception {
// base implementation
};
class ValidationError : public ProjectNameBaseError {
// validation-specific
};
class NetworkError : public ProjectNameBaseError {
// network-specific
};
}
Async and Threading
- Prefer
std::asyncoverstd::thread - Avoid excessive async - not always intuitive in C++
- Use only when truly beneficial
When to use async:
- I/O operations that can block
- CPU-intensive parallel tasks
- Independent operations
- Non-blocking execution
When to avoid:
- Simple operations
- Complex interdependencies
- When synchronous is clearer
- Over-engineering
System Calls
Prefer libraries over system calls:
❌ Avoid:
system("curl -s https://api.example.com/data");
popen("wget -qO- https://api.example.com/data", "r");
execl("/usr/bin/jq", "jq", ".data", "input.json", NULL);
✅ Use libraries:
// Use HTTP library (libcurl, httplib)
auto response = http_client.get("https://api.example.com/data");
// Use JSON library (nlohmann/json, rapidjson)
auto data = json::parse(file_content);
// Use native C++ file operations
std::ifstream file("input.json");
json data;
file >> data;
Benefits:
- Better error handling
- Cross-platform compatibility
- Security (no shell injection)
- Performance (no process spawning)
- Better C++ integration
String Literals
Use constexpr raw strings in headers:
❌ Avoid escaped strings:
const std::string query = "SELECT * FROM users WHERE name = 'John\\'s Data'";
const std::string json = "{\"name\": \"John\", \"age\": 30}";
✅ Use constexpr raw strings:
// In header file
constexpr auto SQL_QUERY = R"(
SELECT * FROM users
WHERE name = 'John's Data'
AND status = "active"
)";
constexpr auto USER_JSON = R"({
"name": "John",
"age": 30,
"city": "New York"
})";
constexpr auto DATE_REGEX = R"(\d{4}-\d{2}-\d{2})";
Benefits:
- Compile-time evaluation
- Readable format
- No escape issues
- Maintainable
Regex and String Processing
Prefer std::regex over manual string operations:
❌ Avoid manual parsing:
bool isValidEmail(const std::string& email) {
if (email.find('@') == std::string::npos) return false;
if (email.find('.') == std::string::npos) return false;
return true;
}
✅ Use std::regex:
// In header file
constexpr auto EMAIL_REGEX = R"([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})";
constexpr auto IP_REGEX = R"((\d{1,3}\.){3}\d{1,3})";
// In implementation
bool isValidEmail(const std::string& email) {
static const std::regex pattern(EMAIL_REGEX);
return std::regex_match(email, pattern);
}
std::vector<std::string> findDates(const std::string& text) {
static const std::regex pattern(R"(\d{4}-\d{2}-\d{2})");
std::vector<std::string> dates;
std::sregex_iterator begin(text.begin(), text.end(), pattern);
std::sregex_iterator end;
for (auto it = begin; it != end; ++it) {
dates.push_back(it->str());
}
return dates;
}
Version Control
Required Files
Always create .gitignore:
# Build artifacts
build/
dist/
out/
# CMake artifacts
CMakeCache.txt
CMakeFiles/
cmake_install.cmake
Makefile
*.cmake
!CMakeLists.txt
# qmake artifacts
*.pro.user
*.qbs.user
moc_*.cpp
moc_*.h
qrc_*.cpp
ui_*.h
object_script_*
# Compiled files
*.o
*.obj
*.so
*.dylib
*.dll
*.exe
*.out
*.app
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
# Temporary files
*.tmp
*.temp
*.log
*.pid
# Dependencies
node_modules/
vendor/
Optional Files
Only create when explicitly requested:
- README.md files
- Shell scripts
- Test files (beyond basic structure)
- Documentation files
Quality Assurance
- All code must compile with C++26 standard
- Follow these conventions strictly
- Update this document when conventions change
- Document exceptions to these rules
- Tests must be runnable with Kyua framework
This guide represents modern C++ development best practices for 2025, with emphasis on the upcoming C++26/C++2c standard. Keep it updated as conventions evolve and new language features become available.