Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Modern C++ (C++11 to C++20)
“Modern C++” refers to the massive evolution of the language starting with C++11. It emphasizes safety, expressiveness, and performance. If you are writing C++ like it’s 1998, you are working too hard.
1. Type Inference (auto)
Let the compiler deduce types for you. It prevents typos, makes refactoring easier, and avoids accidental type mismatches that cause silent performance bugs.
// Before: Verbose, error-prone, and painful to refactor
std::vector<std::string>::iterator it = names.begin();
// After: Clean, correct, and adapts automatically if the container type changes
auto it = names.begin();
// auto also prevents accidental narrowing or type mismatches
std::map<std::string, std::vector<int>> data;
// Without auto, this type is absurdly long and easy to get wrong:
// std::map<std::string, std::vector<int>>::const_iterator cit = data.cbegin();
auto cit = data.cbegin(); // Correct, readable, maintainable
When NOT to use auto: If the deduced type is not obvious from the right-hand side, spell it out. auto x = getResult() is unclear — is it an int? A string? A shared_ptr? Write int x = getResult() when the type matters for understanding the code. The “Almost Always Auto” style has vocal proponents, but readability should be the deciding factor.
2. Smart Pointers (Memory Safety)
We covered this in the Memory chapter, but it bears repeating: Never use new and delete.
std::unique_ptr: The default choice. Fast, safe, exclusive ownership.
std::shared_ptr: Use only when ownership is truly shared.
auto ptr = std::make_unique<MyClass>();
// Automatically deleted when ptr goes out of scope. No leaks!
Before C++11, returning large objects (like a vector with 1M items) involved copying the entire object — allocating new memory, copying every element, then destroying the original. This was painfully slow and led to convoluted code patterns to avoid returning by value.
Move Semantics allow resources to be “stolen” from temporary objects. The analogy: imagine you are moving to a new apartment. Copying is buying identical furniture for the new place and throwing away the old furniture. Moving is loading your existing furniture onto a truck and driving it over. The result is the same — furnished apartment — but moving is dramatically cheaper.
Under the hood, a move operation for a std::vector copies three values (pointer, size, capacity) and sets the source to null. That is O(1) regardless of how many elements the vector holds.
std::vector<int> createVector() {
std::vector<int> v(1'000'000); // 1 million ints (~4MB)
// Fill v with data...
return v; // Move semantics: "steals" v's internal pointer. Zero elements copied.
}
// The caller gets the vector essentially for free
auto data = createVector(); // No copy. The pointer is simply handed over.
When to use std::move explicitly
The compiler automatically moves from temporaries (rvalues). You need std::move to move from a named variable — it tells the compiler “I’m done with this, you can steal from it.”
std::string name = "Alice";
std::vector<std::string> names;
// names.push_back(name); // Copies "Alice" (name is still valid)
names.push_back(std::move(name)); // Moves "Alice" (name is now empty)
// After this line, name is in a "valid but unspecified state"
// Do NOT use name's value -- only reassign or destroy it.
Do not over-use std::move. Moving from a variable and then using it is a bug. Also, do not write return std::move(localVar); — this actually prevents the compiler’s Return Value Optimization (RVO), which is even better than a move. Just write return localVar; and let the compiler optimize.
4. Structured Bindings (C++17)
Allows you to unpack tuples, pairs, and structs directly into variables. It’s similar to destructuring in JavaScript/Python.
std::map<std::string, int> scores = {{"Alice", 10}, {"Bob", 20}};
// Unpack Key and Value directly
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score << "\n";
}
5. std::optional (C++17)
Express that a value might be missing without using “magic numbers” (like returning -1 for “not found”) or null pointers. Before optional, C++ code was riddled with conventions like “return an empty string on failure” or “return -1 if the index doesn’t exist.” These are error-prone because the caller might forget to check. std::optional makes the “might be empty” part of the type itself, so you cannot ignore it.
#include <optional>
std::optional<std::string> findUser(int id) {
if (id == 1) return "Alice";
return std::nullopt; // Explicitly return "nothing" -- no ambiguity
}
auto user = findUser(2);
if (user.has_value()) {
std::cout << user.value();
} else {
std::cout << "User not found";
}
// Concise alternative using value_or() -- provides a default
std::cout << findUser(2).value_or("Unknown"); // Prints "Unknown"
Practical tip: Use optional for function return values that might legitimately have no answer (database lookups, config parsing, search operations). Do not use it for error handling where you need to know why something failed — use exceptions or std::expected (C++23) for that.
6. std::variant (C++17)
A type-safe union. It can hold one of several predefined types, but only one at a time. Unlike C-style union, it tracks which type is currently active and prevents you from reading the wrong one. It is useful for state machines, parsers, AST nodes, and anywhere you have a “this OR that” situation.
#include <variant>
std::variant<int, std::string> data;
data = 10;
std::cout << std::get<int>(data); // 10
data = "Hello";
std::cout << std::get<std::string>(data); // Hello
// std::get<int>(data); // Throws std::bad_variant_access! data holds a string now.
The Visitor Pattern with std::visit
The real power of variant comes from std::visit, which lets you handle each possible type cleanly without chains of if/else or std::get calls.
// A variant representing a parsed config value
using ConfigValue = std::variant<int, double, std::string, bool>;
void printConfig(const ConfigValue& val) {
std::visit([](const auto& v) {
// The compiler generates a version of this lambda for each type
std::cout << v << "\n";
}, val);
}
// More explicit: use an overload set for different handling per type
struct ConfigPrinter {
void operator()(int v) const { std::cout << "Integer: " << v << "\n"; }
void operator()(double v) const { std::cout << "Float: " << v << "\n"; }
void operator()(const std::string& v) const { std::cout << "String: " << v << "\n"; }
void operator()(bool v) const { std::cout << "Bool: " << (v ? "true" : "false") << "\n"; }
};
ConfigValue cfg = "production";
std::visit(ConfigPrinter{}, cfg); // Prints "String: production"
7. Concepts (C++20)
Concepts allow you to constrain template parameters. Before C++20, if you passed the wrong type to a template, you got an incomprehensible error message — sometimes hundreds of lines long, referencing internal implementation details. Concepts fix this by letting you state your requirements upfront.
Think of concepts as a “requirements checklist” for types. Instead of saying “this function takes any type T” and hoping for the best, you say “this function takes any type T that supports addition and comparison.”
#include <concepts>
// Only accept types that are integral (int, long, short, etc.)
template <std::integral T>
T add(T a, T b) {
return a + b;
}
add(10, 20); // OK: int is integral
add(1.5, 2.5); // Compile Error: "double does not satisfy integral" -- clear, one line
// You can define your own concepts
template <typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::same_as<std::ostream&>; // T must support operator<<
};
template <Printable T>
void log(const T& value) {
std::cout << "[LOG] " << value << "\n";
}
Practical impact: Concepts make templates accessible to non-experts. Before C++20, writing template-heavy code was an advanced skill partly because the error messages were so difficult to parse. With concepts, template errors read like English. If you are writing library code or reusable components, concepts should be your default tool for constraining templates.
8. Ranges (C++20)
Ranges allow you to compose algorithms using the pipe operator (|). If you have used Unix shell pipelines (cat file | grep pattern | sort), ranges will feel familiar. Each step transforms the data and passes it to the next step. The key insight: ranges are lazy — they do not create intermediate containers. The filtering, transforming, and taking all happen in a single pass as you iterate.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6};
// Pipeline: Filter evens -> Square them -> Take first 2
// No intermediate vectors are created -- this is a single-pass lazy pipeline
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; }) // 2, 4, 6
| std::views::transform([](int n) { return n * n; }) // 4, 16, 36
| std::views::take(2); // 4, 16
for (int n : result) {
std::cout << n << " "; // 4 16
}
}
Compare this to the pre-ranges equivalent, which requires multiple loops or temporary containers:
// Without ranges: imperative, verbose, and creates a temporary vector
std::vector<int> temp;
for (int n : nums) {
if (n % 2 == 0) {
temp.push_back(n * n);
if (temp.size() == 2) break;
}
}
Ranges make the intent of the code clear while being equally efficient.
The Modern C++ Mindset
Adopting modern C++ is not just about learning new syntax — it is a shift in how you think about writing code:
- Express intent, not mechanism. Use
auto, ranges, and structured bindings so your code reads like a description of what it does, not how it does it.
- Make illegal states unrepresentable. Use
std::optional instead of magic sentinel values. Use std::variant instead of type-unsafe unions. Use enum class instead of raw integers.
- Ownership is a design decision. Every resource in your program should have a clear owner.
unique_ptr for single ownership, shared_ptr for shared ownership, and stack allocation for everything else.
- Zero-cost abstractions are real. Move semantics,
constexpr, and templates compile down to the same machine code you would write by hand. There is no reason to avoid them for “performance.”
Summary
Modern C++ is a different beast from “C with Classes”.
- Safety: Smart pointers and
std::optional eliminate common bugs.
- Expressiveness:
auto, structured bindings, and ranges make code readable.
- Performance: Move semantics ensure abstractions are zero-cost.
- Clarity: Concepts give you readable error messages and self-documenting template interfaces.
You are now equipped with the knowledge to write professional, modern C++.