C++ is a high-performance, multi-paradigm language used in systems programming, game engines, embedded devices, and high-frequency trading. This cheat sheet covers modern C++17/20 — from basics to templates and concurrency — with copy-ready examples.
Quick reference
The 25 patterns that cover 95% of everyday C++ development.
| Pattern | Example |
|---|---|
| Auto type | auto x = 42; |
| Range-for | for (auto& v : vec) |
| Initialiser list | vector<int> v = {1, 2, 3}; |
| Structured binding | auto [a, b] = pair; |
| Lambda | [x](int y) { return x + y; } |
| Unique pointer | auto p = make_unique<T>(args); |
| Shared pointer | auto p = make_shared<T>(args); |
| Move semantics | T t = std::move(other); |
| Perfect forward | std::forward<T>(arg) |
| String literal | std::string s = "hello"s; |
| Optional | std::optional<int> opt = 42; |
| Variant | std::variant<int, string> |
| Fold expression | (args + ...) |
| If constexpr | if constexpr (is_integral_v<T>) |
| Constexpr func | constexpr int sq(int x) |
| Nodiscard | [[nodiscard]] int compute(); |
| Scoped enum | enum class Color { Red, Green }; |
| Nullptr | T* p = nullptr; |
| Override | void f() override; |
| Delete | T(const T&) = delete; |
| Default | T() = default; |
| Template alias | using Vec = vector<T>; |
| Concept (C++20) | template<std::integral T> |
| Span (C++20) | std::span<int> s(arr, 5); |
| Range algo (C++20) | ranges::sort(vec); |
Variables and types
// Fundamental types
int i = 42;
long long ll = 9'000'000'000LL; // digit separators (C++14)
double d = 3.14;
float f = 3.14f;
bool b = true;
char c = 'A';
std::string s = "hello";
// Auto — prefer for complex types
auto x = 3.14; // double
auto v = std::vector<int>{1, 2, 3};
// Const and constexpr
const int MAX = 100; // runtime constant
constexpr int SQ = 5 * 5; // compile-time constant
// References and pointers
int a = 10;
int& ref = a; // lvalue reference — alias for a
int* ptr = &a; // pointer to a
*ptr = 20; // dereference
// Nullptr — always use instead of NULL or 0
int* p = nullptr;
// Structured bindings (C++17)
auto [x2, y2] = std::pair<int, int>{1, 2};
auto [key, val] = std::map<std::string, int>::value_type{"age", 30};
Type aliases
using Name = std::string;
using IntVec = std::vector<int>;
template<typename T>
using Matrix = std::vector<std::vector<T>>;
Strings
#include <string>
#include <string_view>
std::string s = "Hello, World!";
// Size / access
s.size(); // 13
s.length(); // same as size()
s[0]; // 'H'
s.at(0); // 'H' (bounds-checked)
s.front(); // 'H'
s.back(); // '!'
// Modify
s += " More";
s.append(" text");
s.insert(5, " C++");
s.erase(5, 4);
s.replace(0, 5, "Hi");
// Search
s.find("World"); // returns position or string::npos
s.rfind("l");
s.contains("World"); // C++23; use find() != npos for C++17
// Substrings and splitting
s.substr(7, 5); // "World"
// Conversions
std::to_string(42);
std::stoi("42");
std::stod("3.14");
// string_view — non-owning, cheap to copy (prefer for read-only params)
std::string_view sv = s;
sv.substr(0, 5); // no allocation
Control flow
// If / else
if (x > 0) {
// ...
} else if (x < 0) {
// ...
} else {
// ...
}
// If with initialiser (C++17)
if (auto it = map.find(key); it != map.end()) {
std::cout << it->second;
}
// Switch
switch (day) {
case 1: std::cout << "Mon"; break;
case 2: std::cout << "Tue"; break;
default: std::cout << "Other";
}
// Loops
for (int i = 0; i < 10; ++i) { }
// Range-for
std::vector<int> nums{1, 2, 3};
for (auto& n : nums) { n *= 2; } // by reference — modifies elements
for (const auto& n : nums) { } // by const reference — read-only
// While / do-while
while (cond) { }
do { } while (cond);
// Ternary
int abs_val = x >= 0 ? x : -x;
Functions
// Basic function
int add(int a, int b) { return a + b; }
// Default parameters
void greet(std::string name, std::string greeting = "Hello") {
std::cout << greeting << ", " << name;
}
// Overloading
int square(int x) { return x * x; }
double square(double x) { return x * x; }
// Inline — suggest inlining (compiler decides)
inline int max(int a, int b) { return a > b ? a : b; }
// Returning multiple values (use struct or pair/tuple)
std::pair<int, int> divmod(int a, int b) {
return {a / b, a % b};
}
auto [quot, rem] = divmod(17, 5);
// Variadic templates
template<typename... Args>
void print(Args&&... args) {
((std::cout << args << " "), ...); // fold expression (C++17)
}
// Lambdas
auto sq = [](int x) { return x * x; };
int factor = 3;
auto mul = [factor](int x) { return x * factor; }; // capture by value
auto inc = [&factor](int x) { return x + factor; }; // capture by reference
auto gen = [i = 0]() mutable { return i++; }; // mutable capture
Classes and OOP
class Animal {
public:
Animal(std::string name, int age) // constructor
: name_(std::move(name)), age_(age) {} // member initialiser list
// Virtual destructor — always for base classes
virtual ~Animal() = default;
virtual std::string speak() const = 0; // pure virtual → abstract class
// Getters
const std::string& name() const { return name_; }
int age() const { return age_; }
// Static member
static int count() { return count_; }
protected:
std::string name_;
int age_;
private:
inline static int count_ = 0;
};
class Dog : public Animal {
public:
Dog(std::string name, int age) : Animal(std::move(name), age) {}
std::string speak() const override { return "Woof!"; }
};
// Usage
auto d = std::make_unique<Dog>("Rex", 3);
std::cout << d->speak();
Special member functions
class MyClass {
public:
MyClass() = default; // default constructor
MyClass(const MyClass&) = delete; // disable copy constructor
MyClass& operator=(const MyClass&) = delete; // disable copy assignment
MyClass(MyClass&&) noexcept = default; // move constructor
MyClass& operator=(MyClass&&) noexcept = default; // move assignment
~MyClass() = default;
};
Operator overloading
struct Vec2 {
double x, y;
Vec2 operator+(const Vec2& o) const { return {x + o.x, y + o.y}; }
Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
bool operator==(const Vec2& o) const = default; // C++20
};
// Stream output
std::ostream& operator<<(std::ostream& os, const Vec2& v) {
return os << "(" << v.x << ", " << v.y << ")";
}
Smart pointers
Always prefer smart pointers over raw new/delete.
#include <memory>
// unique_ptr — sole ownership, non-copyable
auto up = std::make_unique<std::string>("hello");
up->size(); // dereference with ->
std::string* raw = up.get(); // raw pointer (does not transfer ownership)
up.reset(); // destroy immediately
// shared_ptr — shared ownership, reference-counted
auto sp1 = std::make_shared<int>(42);
auto sp2 = sp1; // reference count = 2
sp1.use_count(); // 2
// weak_ptr — non-owning observer; breaks circular references
std::weak_ptr<int> wp = sp1;
if (auto locked = wp.lock()) { // obtain temporary shared_ptr
std::cout << *locked;
}
// Passing smart pointers to functions
void takeOwnership(std::unique_ptr<Foo> p); // transfers ownership
void borrow(const Foo& ref); // just needs to use it
void share(std::shared_ptr<Foo> p); // shared ownership needed
| Pointer | Use when |
|---|---|
unique_ptr |
One clear owner (default choice) |
shared_ptr |
Multiple owners / shared lifecycle |
weak_ptr |
Observer; breaks cycles with shared_ptr |
Raw T* |
Non-owning reference (never new/delete manually) |
STL containers
#include <vector>
#include <array>
#include <list>
#include <deque>
#include <map>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <stack>
#include <queue>
// --- vector (dynamic array) ---
std::vector<int> v = {3, 1, 4, 1, 5};
v.push_back(9);
v.emplace_back(2); // construct in-place
v.pop_back();
v.size(); // element count
v.empty();
v.reserve(100); // pre-allocate capacity
v.resize(10, 0); // resize, fill new with 0
v.front(); v.back();
v[2]; v.at(2); // at() throws if out of range
v.erase(v.begin() + 2);
v.insert(v.begin(), 0);
// --- array (fixed-size) ---
std::array<int, 5> arr = {1, 2, 3, 4, 5};
arr.fill(0);
// --- unordered_map (hash map) O(1) average ---
std::unordered_map<std::string, int> scores;
scores["Alice"] = 95;
scores.emplace("Bob", 80);
scores.contains("Alice"); // C++20
auto it = scores.find("Alice");
if (it != scores.end()) { std::cout << it->second; }
scores.erase("Bob");
for (auto& [k, v] : scores) { std::cout << k << "=" << v; }
// --- map (sorted tree) O(log n) ---
std::map<std::string, int> m;
m["a"] = 1;
m.lower_bound("b"); // iterator to first key >= "b"
// --- set / unordered_set ---
std::unordered_set<int> s = {1, 2, 3};
s.insert(4);
s.count(3); // 1 if present, 0 if not
s.erase(2);
// --- stack (LIFO) ---
std::stack<int> stk;
stk.push(1); stk.push(2);
stk.top(); // 2
stk.pop();
// --- queue (FIFO) ---
std::queue<int> q;
q.push(1); q.push(2);
q.front(); // 1
q.pop();
// --- priority_queue (max-heap by default) ---
std::priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(4);
pq.top(); // 4
// Min-heap:
std::priority_queue<int, std::vector<int>, std::greater<int>> min_pq;
STL algorithms
#include <algorithm>
#include <numeric>
std::vector<int> v = {5, 3, 1, 4, 2};
// Sort
std::sort(v.begin(), v.end());
std::sort(v.begin(), v.end(), std::greater<int>()); // descending
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
// Ranges (C++20) — no .begin()/.end() needed
std::ranges::sort(v);
std::ranges::sort(v, std::greater{});
// Search
auto it = std::find(v.begin(), v.end(), 3);
bool found = std::binary_search(v.begin(), v.end(), 3); // sorted only
auto [mn, mx] = std::minmax_element(v.begin(), v.end());
// Transform / modify
std::transform(v.begin(), v.end(), v.begin(), [](int x) { return x * 2; });
std::for_each(v.begin(), v.end(), [](int& x) { x += 1; });
std::fill(v.begin(), v.end(), 0);
std::reverse(v.begin(), v.end());
// Copy / remove
std::vector<int> out;
std::copy_if(v.begin(), v.end(), std::back_inserter(out), [](int x) { return x > 2; });
// Erase-remove idiom (pre-C++20)
v.erase(std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; }), v.end());
// C++20:
std::erase_if(v, [](int x) { return x % 2 == 0; });
// Numeric
int sum = std::accumulate(v.begin(), v.end(), 0);
int product = std::accumulate(v.begin(), v.end(), 1, std::multiplies<int>());
std::partial_sum(v.begin(), v.end(), out.begin());
std::iota(v.begin(), v.end(), 1); // fill with 1, 2, 3, ...
// Count / check
int cnt = std::count_if(v.begin(), v.end(), [](int x) { return x > 3; });
bool all = std::all_of(v.begin(), v.end(), [](int x) { return x > 0; });
bool any = std::any_of(v.begin(), v.end(), [](int x) { return x > 10; });
bool none = std::none_of(v.begin(), v.end(), [](int x) { return x < 0; });
Templates
// Function template
template<typename T>
T max_val(T a, T b) { return a > b ? a : b; }
max_val(3, 7); // deduced: int
max_val(3.0, 7.0); // deduced: double
// Class template
template<typename T>
class Stack {
public:
void push(T val) { data_.push_back(std::move(val)); }
T pop() {
T val = std::move(data_.back());
data_.pop_back();
return val;
}
bool empty() const { return data_.empty(); }
private:
std::vector<T> data_;
};
// Template specialisation
template<>
std::string max_val<std::string>(std::string a, std::string b) {
return a.size() > b.size() ? a : b;
}
// Variadic templates
template<typename T>
T sum(T val) { return val; }
template<typename T, typename... Rest>
T sum(T first, Rest... rest) { return first + sum(rest...); }
sum(1, 2, 3, 4); // 10
// Concepts (C++20) — constrain template parameters
template<std::integral T>
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }
template<typename T>
concept Printable = requires(T t) { std::cout << t; };
template<Printable T>
void log(const T& val) { std::cout << val << "\n"; }
// if constexpr — compile-time branching
template<typename T>
void process(T val) {
if constexpr (std::is_integral_v<T>) {
std::cout << "int: " << val;
} else if constexpr (std::is_floating_point_v<T>) {
std::cout << "float: " << val;
} else {
std::cout << "other: " << val;
}
}
Error handling
#include <stdexcept>
#include <optional>
#include <expected> // C++23
// Exceptions
try {
if (x < 0) throw std::invalid_argument("x must be >= 0");
if (x > 100) throw std::out_of_range("x exceeds limit");
// ...
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what();
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what();
} catch (...) {
std::cerr << "Unknown error";
}
// Custom exception
class NetworkError : public std::runtime_error {
public:
explicit NetworkError(const std::string& msg, int code)
: std::runtime_error(msg), code_(code) {}
int code() const { return code_; }
private:
int code_;
};
// Optional — value that may be absent (no exceptions, no null)
std::optional<int> find_value(const std::vector<int>& v, int target) {
auto it = std::find(v.begin(), v.end(), target);
if (it == v.end()) return std::nullopt;
return *it;
}
auto result = find_value(nums, 42);
if (result) {
std::cout << *result;
}
result.value_or(-1); // -1 if empty
// std::expected (C++23) — explicit success/error without exceptions
std::expected<int, std::string> parse_int(std::string_view s) {
try { return std::stoi(std::string(s)); }
catch (...) { return std::unexpected("not a number: " + std::string(s)); }
}
auto val = parse_int("42");
if (val) { std::cout << *val; }
else { std::cout << val.error(); }
Move semantics and rvalue references
// lvalue = named object; rvalue = temporary / expiring value
std::string s1 = "hello";
std::string s2 = s1; // copy — s1 still valid
std::string s3 = std::move(s1); // move — s1 is now in valid-but-unspecified state
// Move constructor / assignment
class Buffer {
public:
explicit Buffer(size_t n) : data_(new int[n]), size_(n) {}
~Buffer() { delete[] data_; }
// Move constructor
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
// Delete copy
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
private:
int* data_;
size_t size_;
};
// Perfect forwarding
template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg)); // forwards lvalue as lvalue, rvalue as rvalue
}
Concurrency (C++11/14/17)
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <future>
// Basic thread
std::thread t([]() { std::cout << "Hello from thread\n"; });
t.join(); // wait for completion
// t.detach(); // let it run independently
// Mutex — protect shared data
std::mutex mtx;
int counter = 0;
void safe_increment() {
std::lock_guard<std::mutex> lock(mtx); // RAII lock
++counter;
}
// unique_lock — more flexible (can unlock/relock)
std::unique_lock<std::mutex> lock(mtx);
lock.unlock();
// ...
lock.lock();
// Atomic — lock-free for simple types
std::atomic<int> atom_counter{0};
atom_counter.fetch_add(1, std::memory_order_relaxed);
int val = atom_counter.load();
// Condition variable
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;
// Producer
{
std::lock_guard<std::mutex> lk(cv_mtx);
ready = true;
}
cv.notify_all();
// Consumer
std::unique_lock<std::mutex> lk(cv_mtx);
cv.wait(lk, []{ return ready; }); // spurious wakeup safe
// std::async / std::future
auto future = std::async(std::launch::async, []() {
return compute_heavy_result();
});
// Do other work...
auto result = future.get(); // blocks until ready
// Multiple futures
std::vector<std::future<int>> futures;
for (int i = 0; i < 4; ++i) {
futures.push_back(std::async(std::launch::async, [i]() { return i * i; }));
}
for (auto& f : futures) { std::cout << f.get() << " "; }
File I/O
#include <fstream>
#include <sstream>
#include <filesystem>
// Read entire file
std::ifstream f("data.txt");
if (!f) throw std::runtime_error("cannot open file");
std::string line;
while (std::getline(f, line)) {
std::cout << line << "\n";
}
// Read all at once
std::ifstream file("data.txt");
std::string content((std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>());
// Write file
std::ofstream out("out.txt");
out << "Line 1\n";
out << "Line 2\n";
// File is closed automatically when out goes out of scope
// Append
std::ofstream app("log.txt", std::ios::app);
app << "New entry\n";
// String streams (in-memory I/O)
std::ostringstream oss;
oss << "Value: " << 42 << ", Pi: " << 3.14;
std::string result = oss.str();
std::istringstream iss("10 20 30");
int a, b, c;
iss >> a >> b >> c;
// std::filesystem (C++17)
namespace fs = std::filesystem;
fs::path p = "dir/file.txt";
p.filename(); // "file.txt"
p.stem(); // "file"
p.extension(); // ".txt"
p.parent_path(); // "dir"
fs::exists(p);
fs::is_directory("dir");
fs::create_directories("a/b/c");
fs::remove("file.txt");
fs::copy("src.txt", "dst.txt");
fs::rename("old.txt", "new.txt");
fs::file_size("data.bin");
// Iterate directory
for (const auto& entry : fs::directory_iterator("dir")) {
std::cout << entry.path() << "\n";
}
// Recursive
for (const auto& entry : fs::recursive_directory_iterator("dir")) {
if (entry.is_regular_file()) std::cout << entry.path() << "\n";
}
Modern C++20 features
// Concepts — readable constraints
template<typename T>
concept Number = std::is_arithmetic_v<T>;
template<Number T>
T multiply(T a, T b) { return a * b; }
// Ranges — composable lazy views
#include <ranges>
namespace views = std::views;
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
auto result = nums
| views::filter([](int n) { return n % 2 == 0; })
| views::transform([](int n) { return n * n; });
// result is lazy — evaluated only when iterated
for (int n : result) { std::cout << n << " "; } // 4 16 36
// Span — non-owning view over contiguous data
#include <span>
void process(std::span<const int> data) {
for (int x : data) std::cout << x;
}
std::vector<int> v = {1, 2, 3};
process(v); // works with vector
int arr[] = {1, 2, 3};
process(arr); // works with C array
// Three-way comparison (spaceship operator)
#include <compare>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default; // generates all 6 comparisons
};
// Coroutines (C++20) — stackless co-routines
#include <coroutine>
// Usually used through libraries (cppcoro, std::generator in C++23)
Common patterns
RAII (Resource Acquisition Is Initialisation)
// Resources tied to object lifetime — no leaks possible
class FileGuard {
public:
explicit FileGuard(const char* path) : f_(std::fopen(path, "r")) {
if (!f_) throw std::runtime_error("cannot open");
}
~FileGuard() { if (f_) std::fclose(f_); }
// Prevent copying
FileGuard(const FileGuard&) = delete;
FileGuard& operator=(const FileGuard&) = delete;
FILE* get() { return f_; }
private:
FILE* f_;
};
// Scope guard — run cleanup on scope exit
template<typename F>
class ScopeGuard {
public:
explicit ScopeGuard(F f) : f_(std::move(f)) {}
~ScopeGuard() { f_(); }
private:
F f_;
};
// Usage:
ScopeGuard cleanup([](){ std::cout << "cleanup!\n"; });
Builder pattern
class HttpRequest {
public:
HttpRequest& url(std::string u) { url_ = std::move(u); return *this; }
HttpRequest& method(std::string m) { method_ = std::move(m); return *this; }
HttpRequest& header(std::string k, std::string v) {
headers_[std::move(k)] = std::move(v); return *this;
}
void send() { /* ... */ }
private:
std::string url_, method_ = "GET";
std::unordered_map<std::string, std::string> headers_;
};
HttpRequest().url("https://api.example.com").method("POST")
.header("Authorization", "Bearer token").send();
Singleton (thread-safe, C++11)
class Config {
public:
static Config& instance() {
static Config inst; // guaranteed thread-safe since C++11
return inst;
}
Config(const Config&) = delete;
Config& operator=(const Config&) = delete;
private:
Config() { /* load config */ }
};
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
delete raw pointer after move |
Use-after-free | Use smart pointers, never delete manually |
Forget virtual destructor in base |
Derived destructor not called | Always add virtual ~Base() = default; |
| Return local variable reference | Dangling reference (UB) | Return by value or use static |
std::vector invalidation in loop |
Iterators invalidated by push_back |
Reserve first or use index loop |
Copy in range-for without & |
Unnecessary copies | Use const auto& or auto& |
| Integer overflow | Silent UB | Use long long or check before arithmetic |
| Unguarded shared data | Data race (UB) | Use mutex or atomic |
| Exception in destructor | std::terminate called |
Mark destructors noexcept, handle inside |
C++ versions quick reference
| Version | Key features |
|---|---|
| C++11 | auto, range-for, lambdas, move semantics, smart pointers, threads, nullptr, constexpr, enum class |
| C++14 | Generic lambdas, make_unique, digit separators, variable templates |
| C++17 | Structured bindings, if constexpr, std::optional/variant/any, std::filesystem, if/switch initialisers |
| C++20 | Concepts, Ranges, Coroutines, std::span, <format>, three-way comparison, modules |
| C++23 | std::expected, std::generator, std::print, std::flat_map, import std; |
C++ vs other languages
| Feature | C++ | Java | Python | Rust |
|---|---|---|---|---|
| Memory management | Manual + RAII | GC | GC | Ownership/borrow |
| Compile target | Native binary | JVM bytecode | Interpreted | Native binary |
| Performance | Highest | High | Low-Medium | Highest |
| Zero-cost abstractions | Yes | No | No | Yes |
| Templates/generics | Templates | Generics (type-erased) | Duck typing | Generics + traits |
| Undefined behaviour | Yes (many cases) | No | No | Prevented by borrow checker |
| Use case | Systems, games, HPC | Enterprise, Android | Scripting, ML | Systems, safety-critical |
FAQ
Q: When should I use C++ instead of Rust or Go?
A: C++ when you need maximum compatibility (decades of libraries), interop with C, mature tooling (MSVC/Clang/GCC), or a team with existing C++ expertise. Rust for new systems code where memory safety matters. Go for services and CLIs where developer speed matters more.
Q: What is undefined behaviour (UB) and why does it matter?
A: UB means the language standard places no constraints on what the compiler does — it can crash, silently produce wrong results, or even appear to work. Common sources: null dereference, signed integer overflow, out-of-bounds access, use-after-free, data races. Use -fsanitize=address,undefined (ASan/UBSan) during development to catch it.
Q: new/delete vs smart pointers — which to use?
A: Smart pointers exclusively in new code. make_unique (sole owner) and make_shared (shared) handle deallocation automatically. Raw new/delete require manual pairing and leak on exceptions. Reserve raw pointers for non-owning references only.
Q: When should I use const&, value, or && for function parameters?
A: Pass by const T& for read-only large objects (strings, containers). Pass by value when the function needs its own copy (then move from it). Pass by T&& (rvalue ref) only when writing constructors/move assignment or perfect-forwarding templates.
Q: How do I avoid include order issues and slow builds?
A: Use forward declarations instead of #include in headers. Prefer #pragma once or proper include guards. For large projects use precompiled headers (PCH) or modules (C++20). Keep implementation in .cpp files, not headers.
Q: What compiler flags should I always use?
A: For development: -Wall -Wextra -Wpedantic -fsanitize=address,undefined -g. For release: -O2 -DNDEBUG (or -O3 for HPC). Enable C++ version explicitly: -std=c++20. Use clang-tidy and clang-format for static analysis and formatting.