Phase 3: Modern C++ Features (C++11 to C++23)

Welcome to Phase 3
You've built a solid foundation in C++ fundamentals and OOP. Now it's time to explore what makes Modern C++ genuinely modern. The C++11 standard was a watershed moment—so transformative that Bjarne Stroustrup (C++'s creator) described it as feeling like a new language. Every major revision since has continued that evolution.
This phase surveys the key features introduced from C++11 through C++23: the features that separate idiomatic modern C++ from old-school C-style C++.
Time commitment: 2-3 weeks, 1-2 hours daily
Prerequisite: Phase 2: Object-Oriented Programming
What You'll Learn
By the end of Phase 3, you'll be able to:
✅ Write expressive lambdas and use them with STL algorithms
✅ Use auto, structured bindings, and range-for loops fluently
✅ Replace raw pointers with smart pointers
✅ Apply C++17 utilities: optional, variant, string_view
✅ Write constrained templates with C++20 Concepts
✅ Use the Ranges library for composable pipelines
✅ Read and understand C++23's std::expected and std::print
1. C++11 Core Features
auto Type Deduction
auto lets the compiler deduce the type from the initializer:
#include <vector>
#include <map>
int main() {
auto x = 42; // int
auto pi = 3.14; // double
auto name = std::string{"Alice"}; // std::string
std::vector<int> nums = {1, 2, 3, 4, 5};
auto it = nums.begin(); // std::vector<int>::iterator
std::map<std::string, int> scores;
// Without auto: std::map<std::string, int>::iterator
auto mit = scores.begin();
}When to use auto:
- Long iterator/template types (always clearer)
- Range-for loop variables
- Lambda return types
- Avoid for simple types where the type is informative (
int count = 0)
auto and references:
std::vector<std::string> names = {"Alice", "Bob"};
// BAD: copies the string
for (auto name : names) { }
// GOOD: reference (no copy)
for (auto& name : names) { }
// GOOD: const reference (read-only)
for (const auto& name : names) { }Range-Based For Loops
Cleaner iteration over any range:
#include <iostream>
#include <vector>
#include <map>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5};
// By value (copy)
for (auto n : nums) {
std::cout << n << ' ';
}
// By const reference (no copy, read-only)
for (const auto& n : nums) {
std::cout << n << ' ';
}
// By reference (modify in-place)
for (auto& n : nums) {
n *= 2;
}
// Works with maps too
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
for (const auto& [name, age] : ages) { // structured binding!
std::cout << name << ": " << age << '\n';
}
}Lambda Expressions
Lambdas are anonymous functions defined inline:
#include <algorithm>
#include <vector>
#include <iostream>
int main() {
// Basic lambda syntax: [capture](params) { body }
auto greet = [](const std::string& name) {
std::cout << "Hello, " << name << "!\n";
};
greet("World");
// Lambda with return value
auto add = [](int a, int b) -> int {
return a + b;
};
std::cout << add(3, 4) << '\n'; // 7
// Using lambdas with STL algorithms
std::vector<int> nums = {5, 2, 8, 1, 9, 3};
// Sort descending
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
// Find first even number
auto it = std::find_if(nums.begin(), nums.end(), [](int n) {
return n % 2 == 0;
});
if (it != nums.end()) {
std::cout << "First even: " << *it << '\n';
}
}Capture modes:
int x = 10;
int y = 20;
// Capture by value (copy of x at definition time)
auto f1 = [x]() { return x; };
// Capture by reference
auto f2 = [&x]() { x += 5; };
// Capture all by value
auto f3 = [=]() { return x + y; };
// Capture all by reference
auto f4 = [&]() { x += y; };
// Mix: x by value, y by reference
auto f5 = [x, &y]() { return x + y; };
// Capture this (inside a class method)
auto f6 = [this]() { return this->member_; };Mutable lambdas (modify captured-by-value variables):
int count = 0;
auto counter = [count]() mutable {
return ++count; // Modifies the COPY of count
};
counter(); // returns 1
counter(); // returns 2
// count is still 0 (the original)nullptr
Replaces the old NULL (which was just 0):
// Old way (ambiguous: is 0 an int or pointer?)
int* p1 = NULL;
// Modern way (unambiguous null pointer constant)
int* p2 = nullptr;
void foo(int x);
void foo(int* p);
// foo(NULL); // Ambiguous! Could call either overload
foo(nullptr); // Always calls foo(int*)constexpr
Compute values at compile time:
// Regular function: computed at runtime
int square(int n) { return n * n; }
// constexpr function: can be computed at compile time
constexpr int square_ce(int n) { return n * n; }
int main() {
constexpr int size = square_ce(10); // Evaluated at compile time!
int arr[size]; // OK: size is a compile-time constant
// Also works at runtime (flexible)
int n;
std::cin >> n;
int result = square_ce(n); // Computed at runtime when n is not constexpr
}constexpr classes (C++14+):
class Point {
public:
constexpr Point(double x, double y) : x_{x}, y_{y} {}
constexpr double x() const { return x_; }
constexpr double y() const { return y_; }
constexpr double distance_from_origin() const {
return x_ * x_ + y_ * y_; // Not sqrt to keep it constexpr
}
private:
double x_, y_;
};
constexpr Point origin{0.0, 0.0};
constexpr Point p{3.0, 4.0};
constexpr double dist = p.distance_from_origin(); // 25.0 at compile timeUniform Initialization
Brace initialization works everywhere, prevents narrowing:
// All of these are "uniform initialization"
int x{42};
double pi{3.14};
std::string s{"hello"};
struct Point {
int x, y;
};
Point p{1, 2}; // Aggregate initialization
std::vector<int> v{1, 2, 3, 4, 5}; // Initializer list
// Prevents narrowing conversions (compile error)
int y = 3.14; // OK: silently truncates to 3
int z{3.14}; // ERROR: narrowing conversion not allowed!std::initializer_list
Accept a brace list as parameter:
#include <initializer_list>
#include <algorithm>
class Stats {
std::vector<double> data_;
public:
Stats(std::initializer_list<double> values)
: data_{values} {}
double max() const {
return *std::max_element(data_.begin(), data_.end());
}
double min() const {
return *std::min_element(data_.begin(), data_.end());
}
};
Stats s{1.5, 2.3, 0.7, 4.1, 3.0};
std::cout << s.max() << '\n'; // 4.1enum class (Scoped Enumerations)
Strongly-typed enums that don't pollute the enclosing scope:
// Old-style enum (pollutes scope, implicit int conversion)
enum Color { RED, GREEN, BLUE };
enum Traffic { RED, YELLOW, GREEN }; // ERROR: RED conflicts!
// Modern scoped enum
enum class Color { Red, Green, Blue };
enum class Traffic { Red, Yellow, Green }; // OK!
Color c = Color::Red;
Traffic t = Traffic::Red;
// No implicit conversion to int
int x = c; // ERROR
int y = static_cast<int>(c); // OK
// Control underlying type
enum class Flags : uint8_t {
None = 0,
Read = 1 << 0,
Write = 1 << 1,
Exec = 1 << 2,
};2. C++11 Smart Pointers
Smart pointers are the modern replacement for raw new/delete. They provide automatic memory management through RAII.
std::unique_ptr
Exclusive ownership. When the unique_ptr is destroyed, the object is deleted.
#include <memory>
class Widget {
public:
Widget(int id) : id_{id} {
std::cout << "Widget " << id_ << " created\n";
}
~Widget() {
std::cout << "Widget " << id_ << " destroyed\n";
}
int id() const { return id_; }
private:
int id_;
};
int main() {
// Create a unique_ptr (prefer make_unique)
auto w1 = std::make_unique<Widget>(1);
std::cout << "w1 id: " << w1->id() << '\n';
// Can't copy, but can move
// auto w2 = w1; // ERROR: can't copy
auto w2 = std::move(w1); // OK: w1 is now null
if (!w1) {
std::cout << "w1 is empty after move\n";
}
// w2 destroyed here (Widget 1 destroyed)
// w1 is null, nothing to destroy
}Factory function pattern:
std::unique_ptr<Shape> makeShape(const std::string& type) {
if (type == "circle") return std::make_unique<Circle>(5.0);
if (type == "rect") return std::make_unique<Rectangle>(3.0, 4.0);
return nullptr;
}
auto shape = makeShape("circle");
if (shape) {
shape->draw();
}std::shared_ptr
Shared ownership. Reference-counted: destroyed when the last owner releases it.
#include <memory>
int main() {
auto sp1 = std::make_shared<Widget>(42);
std::cout << "ref count: " << sp1.use_count() << '\n'; // 1
{
auto sp2 = sp1; // COPY: shared ownership
std::cout << "ref count: " << sp1.use_count() << '\n'; // 2
auto sp3 = sp1;
std::cout << "ref count: " << sp1.use_count() << '\n'; // 3
} // sp2, sp3 destroyed here
std::cout << "ref count: " << sp1.use_count() << '\n'; // 1
// Widget destroyed when sp1 goes out of scope
}When to use each:
| Smart Pointer | Use When |
|---|---|
unique_ptr | Single owner (vast majority of cases) |
shared_ptr | Genuinely shared ownership (multiple owners) |
weak_ptr | Break circular references with shared_ptr |
std::weak_ptr
Non-owning reference to a shared_ptr-managed object. Breaks cycles:
#include <memory>
class Node {
public:
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // weak to break the cycle!
int value;
Node(int v) : value{v} {}
};
int main() {
auto n1 = std::make_shared<Node>(1);
auto n2 = std::make_shared<Node>(2);
n1->next = n2;
n2->prev = n1; // weak_ptr, doesn't increment ref count
// Use weak_ptr: must lock() to get shared_ptr temporarily
if (auto locked = n2->prev.lock()) {
std::cout << "n1 value: " << locked->value << '\n';
}
}3. C++14 Features
C++14 was a refinement release—polishing and extending C++11.
Generic Lambdas
Use auto parameters in lambdas (lambda templates):
// C++11: must specify types
auto add11 = [](int a, int b) { return a + b; };
// C++14: generic lambda with auto parameters
auto add14 = [](auto a, auto b) { return a + b; };
std::cout << add14(1, 2) << '\n'; // int: 3
std::cout << add14(1.5, 2.5) << '\n'; // double: 4.0
std::cout << add14(std::string{"hi"}, std::string{" there"}) << '\n';Return Type Deduction
Functions can deduce their return type from return statements:
// C++11: must specify return type
auto sum11(int a, int b) -> int { return a + b; }
// C++14: deduced from return statement
auto sum14(int a, int b) { return a + b; } // deduces int
// All return paths must return the same type
auto classify(int n) {
if (n > 0) return std::string{"positive"};
if (n < 0) return std::string{"negative"};
return std::string{"zero"};
}std::make_unique
C++11 had std::make_shared but forgot std::make_unique. Fixed in C++14:
// C++11: had to write this yourself
auto p = std::unique_ptr<Widget>(new Widget(42));
// C++14: clean and exception-safe
auto p = std::make_unique<Widget>(42);Binary Literals and Digit Separators
// Binary literals (C++14)
int flags = 0b1010'1100; // 172
// Digit separators for readability
long long million = 1'000'000;
double pi = 3.141'592'653'589'793;
unsigned hex = 0xFF'FF'FF'FF;[[deprecated]] Attribute
Mark functions/types as deprecated with an optional message:
[[deprecated("Use newFunction() instead")]]
void oldFunction() {
// ...
}
void newFunction() {
// ...
}
int main() {
oldFunction(); // Compiler warning: deprecated
newFunction(); // OK
}4. C++17 Features
C++17 brought several highly practical features that simplify everyday code.
Structured Bindings
Unpack tuples, pairs, and structs into named variables:
#include <map>
#include <tuple>
#include <string>
int main() {
// Unpack a pair
std::pair<std::string, int> p{"Alice", 30};
auto [name, age] = p;
std::cout << name << " is " << age << '\n';
// Unpack a tuple
auto t = std::make_tuple(1, 2.5, std::string{"hello"});
auto [i, d, s] = t;
// Unpack struct (aggregate)
struct Point { int x, y; };
Point pt{10, 20};
auto [x, y] = pt;
// Map iteration (very useful!)
std::map<std::string, int> scores = {{"Alice", 95}, {"Bob", 87}};
for (const auto& [student, score] : scores) {
std::cout << student << ": " << score << '\n';
}
// Insert result unpacking
auto [it, inserted] = scores.insert({"Charlie", 92});
if (inserted) {
std::cout << "Inserted: " << it->first << '\n';
}
}if and switch with Initializers
Initialize a variable scoped to the if statement:
#include <map>
#include <string>
std::map<std::string, int> data = {{"key", 42}};
// C++17: init-statement in if
if (auto it = data.find("key"); it != data.end()) {
std::cout << "Found: " << it->second << '\n';
}
// 'it' is not accessible here
// Useful for mutex guards too
if (std::lock_guard lock{mutex}; queue.empty()) {
// ...
}
// Works with switch too
switch (auto ch = getChar(); ch) {
case 'a': // ...
case 'b': // ...
default: break;
}std::optional
Represents a value that may or may not be present. Replaces sentinel values and nullptr:
#include <optional>
#include <string>
// Return optional instead of using -1, null, or exceptions
std::optional<int> parseInt(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::nullopt; // No value
}
}
int main() {
auto result = parseInt("42");
if (result) {
std::cout << "Parsed: " << *result << '\n';
}
// value_or provides a default
auto bad = parseInt("not a number");
std::cout << bad.value_or(-1) << '\n'; // -1
// Optional in function signatures (clearer intent)
std::optional<std::string> findUser(int id);
// Chaining with and_then (C++23), or manual if-chains
if (auto user = findUser(42)) {
std::cout << "User: " << *user << '\n';
}
}std::variant
A type-safe union. Holds exactly one of several possible types:
#include <variant>
#include <string>
#include <iostream>
// A value that can be int, double, or string
using Value = std::variant<int, double, std::string>;
void printValue(const Value& v) {
// std::visit dispatches based on the active type
std::visit([](const auto& val) {
std::cout << val << '\n';
}, v);
}
int main() {
Value v1 = 42;
Value v2 = 3.14;
Value v3 = std::string{"hello"};
printValue(v1); // 42
printValue(v2); // 3.14
printValue(v3); // hello
// Check active type
if (std::holds_alternative<int>(v1)) {
std::cout << "int: " << std::get<int>(v1) << '\n';
}
// Pattern matching style with overloaded visitor
struct Visitor {
void operator()(int n) { std::cout << "int: " << n << '\n'; }
void operator()(double d) { std::cout << "double: " << d << '\n'; }
void operator()(const std::string& s) { std::cout << "string: " << s << '\n'; }
};
std::visit(Visitor{}, v3);
}Variant as error handling:
using Result = std::variant<std::string, std::error_code>;
Result readFile(const std::string& path) {
// If success: return content
// If error: return error code
}std::string_view
A non-owning view into a string. No allocation, no copy:
#include <string_view>
#include <string>
// Old way: always copies
void processOld(const std::string& s);
// Better: no allocation for string literals
void processNew(std::string_view sv) {
std::cout << sv.size() << ": " << sv << '\n';
// sv.substr() returns string_view (no allocation!)
auto first5 = sv.substr(0, 5);
}
int main() {
processNew("hello world"); // No allocation
processNew(std::string{"hi"}); // No copy
const char* c = "raw string";
processNew(c); // Works!
// string_view operations (non-allocating)
std::string_view sv = "Hello, World!";
auto pos = sv.find("World");
if (pos != std::string_view::npos) {
std::cout << sv.substr(pos) << '\n';
}
}Warning: string_view must not outlive the string it references.
[[nodiscard]], [[fallthrough]], [[maybe_unused]]
Standard attributes for better warnings:
// [[nodiscard]]: warn if return value is ignored
[[nodiscard]] int computeResult() { return 42; }
// [[nodiscard]] with message (C++20)
[[nodiscard("Check error code!")]]
std::error_code openFile(const std::string& path);
// [[fallthrough]]: intentional switch fallthrough
switch (n) {
case 1:
doSomething();
[[fallthrough]]; // Intentional!
case 2:
doMore();
break;
}
// [[maybe_unused]]: suppress unused variable warnings
[[maybe_unused]] int debug_flag = 1;
void function([[maybe_unused]] int param) {
// param not used in release build
#ifdef DEBUG
std::cout << param << '\n';
#endif
}std::filesystem
Portable filesystem operations:
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::path p = "/home/user/documents";
// Path operations
std::cout << p.filename() << '\n'; // "documents"
std::cout << p.parent_path() << '\n'; // "/home/user"
std::cout << p.extension() << '\n'; // "" (no extension)
fs::path file = p / "report.txt"; // path concatenation
std::cout << file << '\n'; // "/home/user/documents/report.txt"
// Directory iteration
for (const auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename() << " ("
<< entry.file_size() << " bytes)\n";
}
}
// File operations
fs::create_directories("output/data"); // Create nested dirs
fs::copy("source.txt", "dest.txt");
fs::remove("old_file.txt");
// Check existence
if (fs::exists("config.json")) {
std::cout << "Config found\n";
}
}Fold Expressions
Apply binary operators to variadic template parameter packs:
// Sum of any number of arguments
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // Unary right fold
// Expands to: args1 + (args2 + (args3 + ...))
}
template<typename... Args>
bool allTrue(Args... args) {
return (... && args); // Unary left fold
}
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first;
((std::cout << ", " << rest), ...); // Fold over comma operator
std::cout << '\n';
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
std::cout << allTrue(true, true, false) << '\n'; // false
print(1, "hello", 3.14, 'x'); // 1, hello, 3.14, x
}5. C++20 Features
C++20 was the largest C++ update since C++11, adding four major features (Concepts, Ranges, Coroutines, Modules) plus many smaller ones.
Concepts
Constrain template type parameters with named requirements:
#include <concepts>
// Define a concept: T must support < and ==
template<typename T>
concept Comparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
{ a == b } -> std::convertible_to<bool>;
};
// Use concept to constrain template
template<Comparable T>
T max(T a, T b) {
return (a < b) ? b : a;
}
// Standard library concepts
template<std::integral T>
T factorial(T n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
template<std::floating_point T>
T divide(T a, T b) {
return a / b;
}
// Abbreviated function templates (terse syntax)
auto add(std::integral auto a, std::integral auto b) {
return a + b;
}Built-in standard concepts:
// <concepts>
std::same_as<T, U> // T and U are exactly the same type
std::derived_from<T, U> // T is derived from U
std::convertible_to<T, U>
std::integral<T> // T is an integer type
std::floating_point<T> // T is a floating-point type
std::copyable<T> // T is copyable
std::movable<T> // T is movable
std::invocable<F, Args> // F can be called with ArgsImproved error messages: With concepts, the compiler says "T does not satisfy Comparable" instead of 50 lines of template substitution errors.
Ranges Library
Composable, lazy range algorithms:
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Compose views with | (pipe operator)
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; }) // even numbers
| std::views::transform([](int n) { return n * n; }) // square them
| std::views::take(3); // first 3
for (auto n : result) {
std::cout << n << ' '; // 4 16 36
}
std::cout << '\n';
// Ranges algorithms (no begin/end needed)
std::vector<int> data = {5, 2, 8, 1, 9};
std::ranges::sort(data); // [1, 2, 5, 8, 9]
auto found = std::ranges::find(data, 5);
if (found != data.end()) {
std::cout << "Found: " << *found << '\n';
}
}Useful views:
namespace views = std::views;
// Generate ranges
auto zero_to_9 = views::iota(0, 10); // 0, 1, ..., 9
auto naturals = views::iota(1); // 1, 2, 3, ... (infinite)
// Transform views
auto uppercase = str | views::transform(::toupper);
auto split_words = text | views::split(' ');
// Combine
auto first5evens = views::iota(1)
| views::filter([](int n) { return n % 2 == 0; })
| views::take(5);
// 2, 4, 6, 8, 10 — evaluated lazily!Three-Way Comparison (<=>)
The "spaceship operator" generates all comparison operators:
#include <compare>
struct Version {
int major, minor, patch;
// Define <=> and == gets <, <=, >, >= for free
auto operator<=>(const Version&) const = default;
bool operator==(const Version&) const = default;
};
int main() {
Version v1{1, 2, 3};
Version v2{1, 3, 0};
std::cout << (v1 < v2) << '\n'; // true
std::cout << (v1 > v2) << '\n'; // false
std::cout << (v1 == v2) << '\n'; // false
std::cout << (v1 <= v2) << '\n'; // true
}std::span
Non-owning view over a contiguous sequence (array, vector, raw array):
#include <span>
#include <vector>
#include <iostream>
// Old: pass pointer + size (error-prone)
void printOld(const int* data, size_t size) {
for (size_t i = 0; i < size; ++i)
std::cout << data[i] << ' ';
}
// Modern: span (safe, works with any contiguous container)
void printModern(std::span<const int> data) {
for (const auto& n : data) {
std::cout << n << ' ';
}
std::cout << '\n';
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
int arr[] = {6, 7, 8, 9, 10};
printModern(vec); // Works with vector
printModern(arr); // Works with C array
printModern({arr, 3}); // First 3 elements
printModern(vec); // Full vector
// Span subviews (no allocation)
std::span<int> s{vec};
auto first3 = s.first(3); // [1, 2, 3]
auto last2 = s.last(2); // [4, 5]
auto middle = s.subspan(1, 3); // [2, 3, 4]
}std::format
Python-style string formatting (replaces printf and stringstream):
#include <format>
#include <string>
int main() {
// Basic formatting
std::string s = std::format("Hello, {}!", "World");
std::cout << s << '\n'; // Hello, World!
// Positional arguments
auto msg = std::format("{0} + {1} = {2}", 3, 4, 3 + 4);
std::cout << msg << '\n'; // 3 + 4 = 7
// Format specifiers (similar to printf)
std::cout << std::format("{:>10}", "right") << '\n'; // right-align width 10
std::cout << std::format("{:<10}", "left") << '\n'; // left-align
std::cout << std::format("{:^10}", "center") << '\n'; // center-align
std::cout << std::format("{:.3f}", 3.14159) << '\n'; // 3.142
std::cout << std::format("{:08x}", 255) << '\n'; // 000000ff
std::cout << std::format("{:,}", 1234567) << '\n'; // 1,234,567
// std::print (C++23) — directly prints without intermediate string
}Designated Initializers
Initialize aggregate members by name:
struct Config {
int port = 8080;
std::string host = "localhost";
bool ssl = false;
int timeout = 30;
};
// Old: positional (error-prone, fragile)
Config c1{9090, "example.com", true, 60};
// C++20: designated initializers (clear and order-independent for clarity)
Config c2{
.port = 9090,
.host = "example.com",
.ssl = true,
.timeout = 60,
};
// Only specify what you need; rest get defaults
Config c3{
.port = 3000,
.ssl = true,
};consteval and constinit
// consteval: MUST be evaluated at compile time (unlike constexpr which can be runtime)
consteval int square(int n) {
return n * n;
}
constexpr int a = square(5); // OK: compile time
// int x; square(x); // ERROR: x is not constexpr
// constinit: variable initialized at compile time, but not necessarily const
constinit int counter = 0; // Initialized at compile time
// counter = 5; // OK: it's not const, can be modifiedCoroutines (Basics)
Coroutines are functions that can suspend and resume. C++20 provides the machinery; libraries like cppcoro provide convenient wrappers:
#include <coroutine>
#include <generator> // C++23, or use cppcoro for C++20
// Generator coroutine: produces values lazily
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a; // Suspend and yield value
auto next = a + b;
a = b;
b = next;
}
}
int main() {
auto fibs = fibonacci();
for (int i = 0; i < 10; ++i) {
auto it = fibs.begin();
std::cout << *it << ' ';
++it;
}
}The three coroutine keywords:
co_yield: Suspend and produce a valueco_return: End the coroutine (optionally returning a value)co_await: Suspend until an awaitable completes (used in async I/O)
6. C++23 Features
C++23 continues refining the language with practical additions.
std::expected
Better error handling than exceptions or error codes. Like optional, but carries an error value when empty:
#include <expected>
#include <string>
#include <cerrno>
enum class ParseError { InvalidInput, Overflow, Empty };
std::expected<int, ParseError> parsePositive(std::string_view s) {
if (s.empty()) return std::unexpected(ParseError::Empty);
int result = 0;
for (char c : s) {
if (c < '0' || c > '9')
return std::unexpected(ParseError::InvalidInput);
result = result * 10 + (c - '0');
if (result < 0)
return std::unexpected(ParseError::Overflow);
}
return result;
}
int main() {
auto result = parsePositive("42");
if (result) {
std::cout << "Parsed: " << *result << '\n';
} else {
switch (result.error()) {
case ParseError::InvalidInput: std::cout << "Invalid input\n"; break;
case ParseError::Empty: std::cout << "Empty input\n"; break;
case ParseError::Overflow: std::cout << "Overflow\n"; break;
}
}
// value_or for defaults
std::cout << parsePositive("bad").value_or(-1) << '\n'; // -1
// Monadic operations
auto doubled = parsePositive("21")
.transform([](int n) { return n * 2; })
.value_or(0);
std::cout << doubled << '\n'; // 42
}std::print and std::println
Direct, type-safe printing without std::cout:
#include <print>
int main() {
std::print("Hello, {}!\n", "World"); // No trailing newline
std::println("Hello, {}!", "World"); // Adds newline automatically
std::println("{} + {} = {}", 1, 2, 3);
std::println("{:.2f}", 3.14159); // 3.14
// Print to stderr
std::println(stderr, "Error: {}", "something went wrong");
}Deducing this
Explicit this parameter enables CRTP-free polymorphism and fluent builders:
struct Builder {
std::string name;
int age = 0;
// 'this' is explicit, enabling perfect forwarding
template<typename Self>
auto&& setName(this Self&& self, std::string n) {
self.name = std::move(n);
return std::forward<Self>(self);
}
template<typename Self>
auto&& setAge(this Self&& self, int a) {
self.age = a;
return std::forward<Self>(self);
}
};
Builder b;
b.setName("Alice").setAge(30);std::flat_map and std::flat_set
Sorted, contiguous containers. Better cache performance than tree-based std::map:
#include <flat_map>
#include <flat_set>
int main() {
// Like std::map, but backed by sorted vector
std::flat_map<std::string, int> scores;
scores["Alice"] = 95;
scores["Bob"] = 87;
// Stored in sorted, contiguous memory — fast iteration
// Like std::set, but backed by sorted vector
std::flat_set<int> primes{2, 3, 5, 7, 11};
primes.insert(13);
// O(log n) lookup but better cache behavior than std::set
}if consteval
Detect at compile time whether you're in a constant-evaluated context:
constexpr double computePi() {
if consteval {
// Compile-time path (can use compile-time-only operations)
return 3.14159265358979323846L;
} else {
// Runtime path
return std::numbers::pi;
}
}7. Feature Comparison Across Standards
// Task: Filter even numbers, square them, collect to vector
// C++03 style
std::vector<int> result_03;
for (size_t i = 0; i < nums.size(); ++i) {
if (nums[i] % 2 == 0) {
result_03.push_back(nums[i] * nums[i]);
}
}
// C++11 style
std::vector<int> result_11;
std::copy_if(nums.begin(), nums.end(), std::back_inserter(result_11),
[](int n) { return n % 2 == 0; });
std::transform(result_11.begin(), result_11.end(), result_11.begin(),
[](int n) { return n * n; });
// C++20 style (ranges)
auto result_20 = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::ranges::to<std::vector>();8. What to Adopt First
Not all features need equal attention. Here's a practical adoption order:
Adopt immediately (C++11/14):
| Feature | Why |
|---|---|
auto | Reduces verbosity, avoids type mismatches |
| Range-for | Cleaner iteration, fewer bugs |
| Smart pointers | Eliminate delete and memory leaks |
| Lambdas | Replace function objects and bind |
nullptr | Type-safe null |
override/final | Catch overriding mistakes at compile time |
enum class | Scoped, type-safe enums |
make_unique/make_shared | Exception-safe allocation |
Adopt as needed (C++17):
| Feature | Why |
|---|---|
| Structured bindings | Cleaner pair/tuple/map iteration |
std::optional | Replace null returns and sentinels |
std::string_view | Zero-cost string parameters |
[[nodiscard]] | Prevent ignored error returns |
if with initializer | Scope-limit variables |
Learn and use selectively (C++20):
| Feature | Why |
|---|---|
| Concepts | Better template errors, clearer constraints |
| Ranges | Composable, readable data pipelines |
std::format | Replace printf and stringstream |
| Designated initializers | Clearer struct construction |
<=> | Automatic comparison operators |
Common Mistakes
1. Dangling string_view
// DANGEROUS: string_view outlives the string
std::string_view sv;
{
std::string temp = "hello";
sv = temp; // OK here
} // temp destroyed!
// sv now dangles — undefined behavior to use itFix: Don't store string_view if the string might be destroyed.
2. Capturing Local by Reference in Long-Lived Lambdas
std::function<int()> make_counter_bad() {
int count = 0;
return [&count]() { return ++count; }; // DANGLING: count is gone!
}
std::function<int()> make_counter_good() {
int count = 0;
return [count]() mutable { return ++count; }; // OK: copy!
}3. Using shared_ptr Everywhere
// BAD: shared_ptr when there's only one owner
auto sp = std::make_shared<Widget>(42);
// GOOD: unique_ptr for single ownership
auto up = std::make_unique<Widget>(42);shared_ptr has overhead (reference counting, control block). Use unique_ptr by default.
4. Ignoring [[nodiscard]]
[[nodiscard]] std::error_code writeFile(const std::string& path, const std::string& data);
// WARNING: result ignored
writeFile("output.txt", "content");
// CORRECT: check the result
if (auto err = writeFile("output.txt", "content")) {
std::cerr << "Write failed: " << err.message() << '\n';
}Summary
✅ C++11: auto, range-for, lambdas, smart pointers, nullptr, constexpr, enum class
✅ C++14: Generic lambdas, return type deduction, make_unique, digit separators
✅ C++17: Structured bindings, optional, variant, string_view, filesystem, [[nodiscard]]
✅ C++20: Concepts, Ranges, format, span, spaceship operator, designated initializers
✅ C++23: std::expected, std::print, deducing this, flat_map/flat_set
What's Next?
- Deep Dive: Memory Management & Smart Pointers → —
unique_ptr,shared_ptr, and custom allocators in depth - Deep Dive: STL Containers & Algorithms → — Master the standard library
Additional Resources
- cppreference.com — Definitive C++ reference
- C++ Core Guidelines
- Compiler Explorer (godbolt.org) — See assembly output for any C++ code
- Effective Modern C++ by Scott Meyers — Best book for C++11/14
- C++17 in Detail by Bartłomiej Filipek — Practical C++17 guide
Series: Modern C++ Learning Roadmap
Previous: Phase 2: Object-Oriented Programming
Next: Deep Dive: Memory Management & Smart Pointers
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.