A comprehensive, practical reference that mirrors the flow of LearnCpp while pointing to modern best practices. Use it as a study path and a quick‑look handbook.
- Compiler: GCC, Clang, or MSVC (any reasonably recent C++20 toolchain).
- Editor/IDE: VS Code / CLion / Visual Studio.
- Build system: CMake (learn the basics early).
- Linters/analysis:
clang-tidy, compiler warnings-Wall -Wextra -Wpedantic. - Formatter:
clang-format.
- Strong static typing with value semantics first.
- Objects have storage duration: automatic (stack), dynamic (heap), static.
- References alias an object; pointers hold an address.
- RAII: acquire in constructor, release in destructor.
#include <iostream>
int main() {
std::cout << "Hello, C++!\n";
}Build & run (POSIX):
clang++ -std=c++20 -O2 -Wall -Wextra -pedantic hello.cpp && ./a.outTips
- Treat warnings as errors (
-Werror) while learning. - Step through
main()with a debugger at least once.
- Prefer
constandconstexpr. - Use brace initialization to guard against narrowing:
int n{42}; // ok double d{3.14}; // ok // int bad{3.14}; // error: narrowing
- Locals die at end of scope.
- Never return references/pointers to dead locals.
- Returning by value is cheap for standard types (copy elision, moves).
if/else,switch,for,whilework as expected.- Prefer pre‑increment (
++i) with iterators; same cost for ints.
int add(int a, int b) { return a + b; }
// For large types
std::string greet(const std::string& name) {
return "Hello, " + name;
}Parameter passing heuristics
- Small trivially copyable (int, double, small structs): pass by value.
- Big objects (std::string, std::vector): pass
const&to read, by value if you need a copy anyway,T&&to sink (see §9).
Default arguments live in the declaration; avoid them in virtual interfaces.
- Streams:
std::cout,std::cin,std::cerr. - Formatting (C++20):
<format>is typesafe and fast.
#include <format>
#include <iostream>
int main() {
std::cout << std::format("Pi ≈ {:.3f}\n", 3.1415926);
}int x = 42;
int& r = x; // reference: must bind, cannot be reseated, non-null
int* p = &x; // pointer: holds address, can be reseated or null
*r = 7; // modifies x via r
p = nullptr; // ok for pointers; references can’t be nullPrinting addresses & values:
int v = 10;
int* p = &v;
std::cout << "&p (address of pointer var) = " << &p << '\n';
std::cout << "p (address stored) = " << p << '\n';
std::cout << "*p (value pointed to) = " << *p << '\n';Guidelines
- Avoid owning raw pointers in application code. Prefer
std::unique_ptr. - Use references to express must‑exist relationships.
std::vector<T>– dynamic array (default choice).std::array<T,N>– fixed size on stack.std::string– UTF‑8 by convention in most projects.std::unordered_map<K,V>– hash map;std::map<K,V>– tree map (ordered).std::optional<T>– maybe‑a‑value (better than sentinel values).
#include <algorithm>
#include <vector>
std::vector<int> v{5,2,4,1,3};
std::sort(v.begin(), v.end());Ranges (C++20):
#include <ranges>
#include <vector>
auto odds_squared = v
| std::views::filter([](int x){ return x % 2; })
| std::views::transform([](int x){ return x*x; });#include <cstdio>
class File {
std::FILE* f{};
public:
explicit File(const char* path, const char* mode)
: f(std::fopen(path, mode)) {}
~File() { if (f) std::fclose(f); }
File(const File&) = delete; // non-copyable
File& operator=(const File&) = delete;
File(File&& other) noexcept : f(other.f) { other.f = nullptr; }
File& operator=(File&& other) noexcept {
if (this != &other) {
if (f) std::fclose(f);
f = other.f; other.f = nullptr;
}
return *this;
}
};RAII tips
- If your type owns a resource, either:
- Delete copy ops and implement moves, or
- Define correct copy/move semantics (the Rule of Five).
- Don’t leak exceptions from destructors.
#include <memory>
auto p = std::make_unique<int>(42); // sole ownership
auto q = std::move(p); // transfer ownership
auto sp = std::make_shared<int>(7); // shared ownership (use sparingly)
std::weak_ptr<int> wp = sp; // observe without owningHeuristics
- Default to
unique_ptr. - Reach for
shared_ptronly when shared lifetime is truly required. - Use
weak_ptrto break reference cycles.
#include <string>
#include <vector>
std::string make_big() { return std::string(1'000'000, 'x'); }
int main() {
std::vector<std::string> v;
v.push_back(make_big()); // moved into the vector (NRVO + move)
}Parameter rules of thumb
- Read‑only:
const T&. - Taking ownership (sink):
Tby value (copy elision) orT&&with perfect forwarding. - Use
std::moveto express transfer-of-resources; don’t overuse it.
template <typename T>
T add(T a, T b) { return a + b; }
#include <concepts>
template <std::integral I>
I inc(I x) { return x + 1; }Notes
- Start from function templates → class templates → constraints.
- Concepts make diagnostics clearer and intent explicit.
#include <algorithm>
#include <vector>
int main() {
std::vector<int> xs{1,2,3,4,5};
auto n_odds = std::count_if(xs.begin(), xs.end(), [](int x){ return x % 2; });
}Captures
[=]copy,[&]reference,[this]capture object,[x = expr]init‑capture.
- Return types for expected absence:
std::optional, orstd::expected<T,E>(if available). - Exceptions for exceptional, non‑local conditions.
- Don’t throw from destructors; use
noexceptwhere appropriate.
Example with optional:
#include <optional>
#include <charconv>
std::optional<int> to_int(std::string_view s) {
int x{};
auto [p, ec] = std::from_chars(s.data(), s.data()+s.size(), x);
if (ec == std::errc{}) return x;
return std::nullopt;
}- Modules reduce header bloat; require compiler & build support.
- Ranges offer composable pipelines & projections.
- Coroutines enable async/generator patterns (advanced topic).
- Use
std::jthread(C++20) for scoped threads. - Prefer high‑level concurrency: thread pools, tasks, async libraries.
- Synchronization:
std::mutex,std::lock_guard,std::atomic<T>.
#include <filesystem>
namespace fs = std::filesystem;
for (auto& p : fs::directory_iterator(".")) {
// p.path()
}#include <chrono>
using namespace std::chrono_literals;
auto start = std::chrono::steady_clock::now();
// work
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);project/
├─ CMakeLists.txt
├─ include/ # public headers
├─ src/ # library/app sources
├─ apps/ # small executables
├─ tests/ # unit tests (Catch2 / doctest)
└─ cmake/ # CMake helpers
Example CMakeLists.txt
cmake_minimum_required(VERSION 3.22)
project(hello LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
add_executable(hello src/main.cpp)Testing sketch (doctest)
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest.h>
int add(int a,int b){return a+b;}
TEST_CASE("add") { CHECK(add(2,3)==5); }- Pointers/refs: implement
swap(T&,T&)and afindreturningT*ornullptr. - RAII: timer class that prints elapsed time in destructor.
- Algorithms: sort records by field with
std::ranges::sort+ projection. - Errors: CSV parser returning
std::optional<Record>. - Templates: tiny
minconstrained by a strict ordering concept. - CMake: split into library + tests, then add
install(TARGETS ...)andcpack.
| Pitfall | Better |
|---|---|
| Owning raw pointers | std::unique_ptr, containers manage memory |
new/delete in app code |
avoid; use RAII wrappers |
using namespace std; in headers |
never; prefer qualified names |
Missing virtual destructors in polymorphic base |
make base destructor virtual |
| Copy when you meant move | use std::move intentionally |
| Header include bloat | forward‑declare; consider modules |
| Exceptions for routine absence | use std::optional / expected |
- Small PODs → by value
- Large read‑only →
const& - Sink/own → by value or
T&&+std::forward
If you manage a resource (file, socket, heap pointer): dtor, copy/move ctors, copy/move assigns.
-std=c++20 -O2 -Wall -Wextra -Wpedantic -Wconversion
<array> <vector> <string> <string_view> <optional> <variant> <span><algorithm> <ranges> <numeric> <iterator><memory> <utility> <type_traits><filesystem> <chrono> <format>
A. Foundations (2 weeks)
- Build toolchain, variables & I/O, functions, control flow.
B. Types & Collections (2 weeks)
- Arrays,
std::vector,std::string, enums, references/pointers.
C. Abstraction (2 weeks)
- Classes/structs, RAII, constructors, overloading, namespaces.
D. Generic programming (2–3 weeks)
- Templates, algorithms, ranges, lambdas, ownership models, error handling.
E. Modern features + build (2–3 weeks)
- Concepts, modules (if available), coroutines (read‑through), testing, CMake basics.
Daily rhythm: 45–90 min + 3–5 katas. Weekly: one integration task (refactor, add tests, package).
#include <variant>
#include <string>
#include <iostream>
using V = std::variant<int, std::string>;
int main(){
V v = std::string{"hi"};
std::visit([](auto&& x){ std::cout << x << '\n'; }, v);
}void log(std::string_view s);
log("literal");
std::string name = "Ada";
log(name); // no allocation#include <ranges>
#include <vector>
#include <algorithm>
struct Item{int id; int score;};
std::vector<Item> items{{1,50},{2,70},{3,60}};
std::ranges::sort(items, std::less{}, &Item::score);#include <chrono>
auto t0 = std::chrono::steady_clock::now();
// ... work ...
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0).count();#include <fstream>
#include <string>
#include <vector>
std::vector<std::string> read_lines(const std::string& path){
std::ifstream ifs(path);
std::vector<std::string> out;
for(std::string line; std::getline(ifs, line); ) out.push_back(line);
return out;
}- RAII: tie resource lifetime to object lifetime.
- Copy elision: compiler skips creating temporary copies.
- Move: transfer resources from one object to another.
- SFINAE: substitution failure is not an error; basis for constraints.
- UB: undefined behavior; avoid by following the rules.
- Concurrency and parallel algorithms (
<execution>). - Networking (Asio / Networking TS).
- Metaprogramming with concepts &
constexpralgorithms. - Performance profiling, allocators, cache‑friendly data layouts.
- Set a breakpoint in
main. - Step into/over; inspect variables and call stack.
- Learn watch expressions and conditional breakpoints.
- Run with a baseline config:
Checks: '-*,bugprone-*,performance-*,readability-*,cppcoreguidelines-*' WarningsAsErrors: '*'
- Iterate by enabling more checks gradually.
// math.ixx
export module math;
export int add(int a, int b) { return a+b; }// main.cpp
import math;
#include <iostream>
int main(){ std::cout << add(2,3) << '\n'; }Build system support for modules is evolving; check your compiler & CMake version.
End of Guide