Modern C++ (C++11 / 14 / 17 / 20)
C++ has transformed dramatically since C++11. Modern C++ looks and feels like a different language β safer, more expressive, and more powerful.
Table of Contents
1 β C++11 β The Revolution
- 1.1 auto, Range-For, Uniform Init
- 1.2 nullptr & enum class
- 1.3 Lambda Expressions
- 1.4 constexpr & static_assert
- 1.5 Variadic Templates & tuple
2 β C++14 β Polish
3 β C++17 β Major Feature Update
- 3.1 Structured Bindings
- 3.2 If/Switch with Initializer
- 3.3 optional, variant, any
- 3.4 string_view
- 3.5 filesystem
- 3.6 Fold Expressions & CTAD
- 3.7 constexpr if & Parallel Algorithms
4 β C++20 β The Next Revolution
- 4.1 Concepts
- 4.2 Ranges
- 4.3 std::format
- 4.4 consteval, constinit, Coroutines
- 4.5 Spaceship Operator & span
- 4.6 Modules
5 β Feature Timeline
Glossary β Key Terms at a Glance
| Term | Meaning |
|---|---|
auto |
Compiler deduces the type β less verbose, fewer errors |
| Lambda | Anonymous function object defined inline β capturable |
constexpr |
Evaluate at compile time if possible |
consteval |
Must evaluate at compile time (C++20) |
optional |
Value that may or may not exist β replaces sentinel values |
variant |
Type-safe union β holds one of several types |
string_view |
Non-owning, zero-copy view of a string |
| Concept | Named constraint on template parameters (C++20) |
| Range | Composable lazy pipeline of operations (C++20) |
| CTAD | Class Template Argument Deduction β compiler deduces template args |
| Coroutine | Function that can suspend and resume execution |
1 β C++11 β The Revolution
1.1 auto, Range-For, Uniform Init
Auto Type Deduction:
auto x = 42; // int
auto y = 3.14; // double
auto s = std::string("hello"); // std::string
auto it = myMap.begin(); // complex iterator type deduced
// Trailing return type (useful with templates)
auto add(int a, int b) -> int { return a + b; }
Range-Based For Loops:
std::vector<int> v{1, 2, 3, 4, 5};
for (auto x : v) { /* copy */ }
for (auto& x : v) { /* reference, modifiable */ }
for (const auto& x : v) { /* const ref β PREFERRED for reading */ }
Uniform Initialization:
int x{42};
std::vector<int> v{1, 2, 3};
std::map<std::string, int> m{{"a", 1}, {"b", 2}};
Point p{3.0, 4.0};
// Prevents narrowing conversions
int x{3.14}; // ERROR β narrowing from double to int
1.2 nullptr & enum class
// nullptr β type-safe null pointer
int* p = nullptr; // replaces NULL and 0
// Resolves ambiguity:
void foo(int);
void foo(int*);
foo(nullptr); // calls foo(int*) β correct!
foo(NULL); // might call foo(int) β ambiguous!
// enum class β scoped, strongly-typed enums
enum class Color : uint8_t { Red, Green, Blue };
Color c = Color::Red;
// No implicit conversion to int β safe!
// int x = c; // ERROR
1.3 Lambda Expressions
Definition: A lambda is an anonymous function object defined inline. It can capture variables from the enclosing scope.
auto add = [](int a, int b) { return a + b; };
add(3, 4); // 7
// Capture variables
int factor = 10;
auto scale = [factor](int x) { return x * factor; }; // capture by value
auto modify = [&factor](int x) { factor = x; }; // capture by reference
// Capture modes
auto f1 = [=]() { /* all by value */ };
auto f2 = [&]() { /* all by reference */ };
auto f3 = [=, &x]() { /* all by value, x by reference */ };
π Why Lambdas Matter: They replace verbose functor classes, enable clean algorithm usage (
std::sortwith custom comparator), and are the foundation of modern C++ functional style.
1.4 constexpr & static_assert
constexpr int factorial(int n) {
return (n <= 1) ? 1 : n * factorial(n - 1);
}
constexpr int f10 = factorial(10); // computed at COMPILE TIME
static_assert(sizeof(int) == 4, "int must be 4 bytes");
static_assert(std::is_integral_v<int>); // C++17
1.5 Variadic Templates & tuple
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << "\n"; // C++17 fold expression
}
print(1, " hello ", 3.14); // "1 hello 3.14"
// std::tuple
auto t = std::make_tuple(1, "hello", 3.14);
auto [a, b, c] = t; // C++17 structured bindings
std::get<0>(t); // 1
2 β C++14 β Polish
2.1 Generic Lambdas & make_unique
// Generic lambdas β auto parameters
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.5, 2.5); // double
// Return type deduction
auto multiply(int a, int b) {
return a * b; // return type deduced as int
}
// make_unique (wasn't in C++11!)
auto p = std::make_unique<MyClass>(arg1, arg2);
2.2 Binary Literals & Digit Separators
int binary = 0b1010'1100; // binary literal with separators
int million = 1'000'000; // readable!
double pi = 3.141'592'653;
[[deprecated("Use newFunction() instead")]]
void oldFunction() { /* ... */ }
3 β C++17 β Major Feature Update
3.1 Structured Bindings
Definition: Structured bindings let you decompose aggregates (structs, pairs, tuples, arrays) into named variables in a single declaration.
std::map<std::string, int> m{{"alice", 90}, {"bob", 85}};
for (const auto& [name, score] : m) {
std::cout << name << ": " << score << "\n";
}
auto [x, y] = std::make_pair(1, 2);
// Works with arrays too
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;
3.2 If/Switch with Initializer
if (auto it = m.find("alice"); it != m.end()) {
// use it->second
// it is scoped to this if-else block
}
switch (auto val = compute(); val) {
case 0: /* ... */ break;
case 1: /* ... */ break;
}
Why this matters: The variable is scoped to the if/switch block β no leaking into the outer scope. Cleaner and safer code.
3.3 optional, variant, any
std::optional β Nullable value:
#include <optional>
std::optional<int> findUser(const std::string& name) {
if (/* found */) return 42;
return std::nullopt;
}
auto result = findUser("alice");
if (result) {
std::cout << *result;
}
result.value_or(-1); // default value if empty
std::variant β Type-safe union:
#include <variant>
std::variant<int, double, std::string> data;
data = 42;
data = 3.14;
data = "hello";
// Visit pattern
std::visit([](auto&& arg) {
std::cout << arg << "\n";
}, data);
if (std::holds_alternative<int>(data)) {
int val = std::get<int>(data);
}
std::any β Type-erased container:
#include <any>
std::any a = 42;
a = std::string("hello");
a = 3.14;
double d = std::any_cast<double>(a); // 3.14
3.4 string_view
Definition: std::string_view is a non-owning, zero-copy reference to a character sequence. Just a pointer + size.
#include <string_view>
void process(std::string_view sv) {
// No allocation, no copy β just a pointer + size
sv.substr(0, 5); // O(1) β returns another string_view
sv.find("hello");
sv.size();
}
process("hello world"); // no std::string allocation
std::string s = "hello";
process(s); // no copy
process(std::string_view(s).substr(0, 3)); // "hel" β no allocation
π Use
string_viewinstead ofconst std::string&in function parameters when you donβt need ownership. It accepts bothstd::stringand C-strings without allocation.
β οΈ Danger: string_view does NOT own the data. If the underlying string is destroyed, the string_view becomes a dangling reference. Never store a string_view that outlives its source.
3.5 filesystem
#include <filesystem>
namespace fs = std::filesystem;
fs::path p = "/home/user/data.csv";
p.filename(); // "data.csv"
p.extension(); // ".csv"
p.parent_path(); // "/home/user"
fs::exists(p);
fs::file_size(p);
fs::create_directories("/tmp/a/b/c");
for (const auto& entry : fs::directory_iterator("/home/user")) {
std::cout << entry.path() << "\n";
}
3.6 Fold Expressions & CTAD
Fold Expressions (replace recursive variadic templates):
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // right fold: a + (b + (c + d))
}
template<typename... Args>
void printAll(Args... args) {
(std::cout << ... << args); // left fold
}
sum(1, 2, 3, 4); // 10
Class Template Argument Deduction (CTAD):
std::pair p{1, 2.0}; // deduced as pair<int, double>
std::vector v{1, 2, 3}; // deduced as vector<int>
std::optional o{42}; // deduced as optional<int>
3.7 constexpr if & Parallel Algorithms
constexpr if β compile-time branching:
template<typename T>
auto process(T val) {
if constexpr (std::is_integral_v<T>) {
return val * 2;
} else if constexpr (std::is_floating_point_v<T>) {
return val * 2.5;
} else {
return val;
}
}
// Branches that don't match are NOT compiled β no SFINAE needed!
Parallel Algorithms:
#include <algorithm>
#include <execution>
std::vector<int> v(10'000'000);
std::sort(std::execution::par, v.begin(), v.end()); // parallel sort!
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
[](int& x) { x *= 2; });
[[nodiscard]] attribute (C++17): Forces callers to handle return values:
[[nodiscard]] int compute() { return 42; }
compute(); // WARNING: return value discarded
4 β C++20 β The Next Revolution
4.1 Concepts
Definition: Concepts are named constraints for template parameters. They replace SFINAE with readable, first-class syntax.
#include <concepts>
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
template<Numeric T>
T add(T a, T b) { return a + b; }
// Or with requires clause
template<typename T>
requires std::integral<T>
T multiply(T a, T b) { return a * b; }
// Or abbreviated
auto divide(std::floating_point auto a, std::floating_point auto b) {
return a / b;
}
π Why Concepts? SFINAE errors produce 100-line error messages. Concepts produce clear, one-line diagnostics: βT does not satisfy Numericβ.
4.2 Ranges
#include <ranges>
namespace rv = std::views;
std::vector<int> v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Composable, lazy operations
auto result = v
| rv::filter([](int x) { return x % 2 == 0; }) // even numbers
| rv::transform([](int x) { return x * x; }); // square them
for (int x : result) {
std::cout << x << " "; // 4 16 36 64 100
}
// No intermediate containers created!
π Why Ranges? They compose like Unix pipes. Lazy evaluation means no temporary vectors. Cleaner than chaining
std::transform+std::copy_ifmanually.
4.3 std::format
#include <format>
std::string s = std::format("Hello, {}! You are {} years old.", name, age);
std::string f = std::format("{:.2f}", 3.14159); // "3.14"
std::string h = std::format("{:#x}", 255); // "0xff"
std::string w = std::format("{:>10}", "right"); // " right"
4.4 consteval, constinit, Coroutines
consteval β guaranteed compile-time:
consteval int square(int x) { return x * x; }
int a = square(5); // OK: evaluated at compile time
int b = 5;
// int c = square(b); // ERROR: b is not constexpr
constinit β no static initialization order fiasco:
constinit int global = 42; // guaranteed initialized at compile time
Coroutines:
#include <coroutine>
// Generator pattern
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto temp = a;
a = b;
b = temp + b;
}
}
for (int x : fibonacci() | std::views::take(10)) {
std::cout << x << " "; // 0 1 1 2 3 5 8 13 21 34
}
4.5 Spaceship Operator & span
Three-Way Comparison (Spaceship Operator):
#include <compare>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default;
// Automatically generates: ==, !=, <, >, <=, >=
};
Point a{1, 2}, b{3, 4};
if (a < b) { /* ... */ }
std::span β non-owning view of contiguous data:
#include <span>
void process(std::span<int> data) {
for (int x : data) { /* ... */ }
data.size();
data[0];
data.subspan(2, 3); // slice
}
int arr[] = {1, 2, 3, 4, 5};
std::vector<int> v{1, 2, 3};
process(arr); // works
process(v); // works
4.6 Modules
// math.cppm
export module math;
export int add(int a, int b) { return a + b; }
export int multiply(int a, int b) { return a * b; }
// main.cpp
import math;
import <iostream>;
int main() {
std::cout << add(3, 4); // 7
}
Modules replace #include β faster compilation, no header file issues, no macro pollution.
5 β Feature Timeline
5.1 Feature Timeline Summary
| Feature | C++11 | C++14 | C++17 | C++20 |
|---|---|---|---|---|
auto |
β | Enhanced | β | β |
| Lambdas | β | Generic | β | Template |
| Move semantics | β | β | β | β |
| Smart pointers | β | make_unique |
β | β |
constexpr |
Basic | Relaxed | if constexpr |
consteval |
| Structured bindings | β | β | β | β |
optional/variant |
β | β | β | β |
string_view |
β | β | β | β |
| Concepts | β | β | β | β |
| Ranges | β | β | β | β |
std::format |
β | β | β | β |
| Coroutines | β | β | β | β |
| Modules | β | β | β | β |
Practice Questions
Q1. Explain auto type deduction. When does it deduce a reference? When does it decay? Give examples with const auto& and auto&&.
Q2. Write a lambda that captures a std::vector by reference, sorts it, and returns the median. What happens if you capture by value instead?
Q3. What is std::optional? How does it improve on returning sentinel values like -1 or nullptr? Write a function that uses it.
Q4. Compare std::string vs std::string_view. When is each appropriate? What dangers does string_view have?
Q5. Explain constexpr if with an example. How does it differ from a regular if? How does it replace SFINAE?
Q6. What are C++20 Concepts? Write a Sortable concept that constrains a template to types that support < comparison.
Q7. Write a Ranges pipeline that takes a vector of integers, filters out negatives, squares the remaining, and collects the top 5 results.
Q8. Explain the Spaceship Operator (<=>). What do strong_ordering, weak_ordering, and partial_ordering mean?
Q9. What problem do Modules solve? How do they improve compilation time compared to #include?
Q10. You have a C++03 codebase. List 10 modern C++ features youβd adopt first and why.
Key Takeaways
- Write C++17 at minimum β structured bindings,
optional,string_vieware essentials - Use
autoliberally β reduces verbosity, prevents implicit conversions - Use
string_viewfor function parameters β zero-cost string passing - Use
constexpreverything possible β catch errors at compile time - Concepts replace SFINAE β much more readable template constraints
- Ranges compose beautifully β pipelines of lazy transformations
std::formatreplacesprintfandiostreamformatting β type-safe, readable