Chapter 5: Modern C++ Features (C++11 through C++23)
C++ has evolved dramatically since C++11. This chapter covers the most important features from each standard.
C++11: The Modern C++ Revolution
auto Type Inference
// Compiler deduces type from initializer
auto x = 42; // int
auto y = 3.14; // double
auto z = "hello"; // const char*
auto s = std::string("hello"); // std::string
// Essential for complex types
std::map<std::string, std::vector<int>> myMap;
auto it = myMap.begin(); // vs std::map<std::string, std::vector<int>>::iterator
// auto with references and const
int value = 42;
auto a = value; // int (copy)
auto& b = value; // int& (reference)
const auto& c = value; // const int& (const reference)
auto* d = &value; // int* (pointer)
Range-Based For Loops
std::vector<int> nums = {1, 2, 3, 4, 5};
// By value (copy)
for (int n : nums) {
std::cout << n << " ";
}
// By reference (modify)
for (int& n : nums) {
n *= 2;
}
// By const reference (read efficiently)
for (const int& n : nums) {
std::cout << n << " ";
}
// With auto
for (const auto& n : nums) {
std::cout << n << " ";
}
Lambda Expressions
// Basic lambda
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << "\n"; // 7
// Capture by value [=]
int multiplier = 10;
auto times = [=](int x) { return x * multiplier; };
// Capture by reference [&]
int counter = 0;
auto increment = [&]() { counter++; };
increment();
std::cout << counter << "\n"; // 1
// Mixed captures
int a = 1, b = 2, c = 3;
auto mixed = [a, &b, &c]() { // a by value, b and c by reference
// a++; // ERROR: captured by value (immutable by default)
b++;
c++;
};
// Mutable lambda (can modify value captures)
auto mutableLambda = [a]() mutable {
a++; // OK with mutable
return a;
};
// Generic lambda (C++14)
auto print = [](const auto& x) { std::cout << x << "\n"; };
print(42);
print("hello");
print(3.14);
// Lambda with explicit return type
auto divide = [](int a, int b) -> double {
return static_cast<double>(a) / b;
};
Smart Pointers
#include <memory>
// unique_ptr - exclusive ownership
auto p1 = std::make_unique<int>(42); // C++14
std::unique_ptr<int> p2(new int(42)); // C++11
// shared_ptr - shared ownership
auto p3 = std::make_shared<int>(42);
std::shared_ptr<int> p4 = p3; // Reference count: 2
// weak_ptr - non-owning observer
std::weak_ptr<int> wp = p3;
if (auto locked = wp.lock()) {
std::cout << *locked << "\n";
}
Move Semantics and Rvalue References
// Rvalue reference: binds to temporaries
void process(std::string&& str) {
// str is now "owned" by this function
data = std::move(str); // Transfer ownership
}
std::string s = "hello";
process(std::move(s)); // s is now empty
process("temporary"); // Works with temporaries
// Perfect forwarding
template<typename T>
void wrapper(T&& arg) {
actualFunction(std::forward<T>(arg));
}
nullptr
// Modern null pointer constant
int* p = nullptr; // Clear and type-safe
// Old way (avoid)
int* q = NULL; // Macro, can cause issues
int* r = 0; // Works but unclear
Strongly-Typed Enums
// Old enum (unscoped)
enum Color { Red, Green, Blue }; // Pollutes namespace
int x = Red; // Implicit conversion to int
// New enum class (scoped)
enum class Color { Red, Green, Blue };
Color c = Color::Red; // Must use scope
// int x = Color::Red; // ERROR: no implicit conversion
int x = static_cast<int>(Color::Red); // Explicit conversion required
// With underlying type
enum class Status : uint8_t {
OK = 0,
Error = 1,
Pending = 2
};
constexpr
// Compile-time computation
constexpr int square(int x) {
return x * x;
}
constexpr int result = square(5); // Computed at compile time
int arr[square(3)]; // Array of size 9
// constexpr variables
constexpr double PI = 3.14159265358979;
constexpr int MAX_SIZE = 1024;
Initializer Lists
#include <initializer_list>
class IntVector {
std::vector<int> data;
public:
IntVector(std::initializer_list<int> list) : data(list) {}
};
IntVector v = {1, 2, 3, 4, 5}; // Uses initializer_list constructor
// Works with STL containers
std::vector<int> nums = {1, 2, 3};
std::map<std::string, int> ages = {{"Alice", 30}, {"Bob", 25}};
Variadic Templates
// Accept any number of arguments of any types
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << "\n"; // Fold expression (C++17)
}
print(1, 2, 3); // "123"
print("a", 1, 3.14); // "a13.14"
// Recursive variadic template (C++11 way)
template<typename T>
void printOld(T t) {
std::cout << t << "\n";
}
template<typename T, typename... Args>
void printOld(T t, Args... args) {
std::cout << t << " ";
printOld(args...);
}
C++14: Refinements
Generic Lambdas
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.5, 2.5); // double
add(std::string("a"), std::string("b")); // string
Return Type Deduction
auto multiply(int a, int b) {
return a * b; // Return type deduced as int
}
// Works with complex return types
auto makeVector() {
return std::vector<int>{1, 2, 3};
}
Variable Templates
template<typename T>
constexpr T pi = T(3.14159265358979323846);
double d = pi<double>;
float f = pi<float>;
Binary Literals and Digit Separators
int binary = 0b1010'1010; // Binary literal with separator
int million = 1'000'000; // Easier to read
long long big = 1'000'000'000LL;
std::make_unique
// C++14 added make_unique (make_shared existed in C++11)
auto ptr = std::make_unique<MyClass>(arg1, arg2);
C++17: Major Additions
Structured Bindings
// Decompose pairs, tuples, structs, arrays
std::pair<int, std::string> p = {42, "hello"};
auto [num, str] = p; // num = 42, str = "hello"
std::map<std::string, int> map = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : map) {
std::cout << key << ": " << value << "\n";
}
struct Point { double x, y, z; };
Point pt = {1.0, 2.0, 3.0};
auto [x, y, z] = pt;
// Arrays
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;
if/switch with Initializer
// Initialize variable in if statement
if (auto it = map.find("key"); it != map.end()) {
std::cout << it->second << "\n";
}
// it is out of scope here
// Works with switch too
switch (auto val = compute(); val) {
case 0: break;
case 1: break;
default: break;
}
std::optional
#include <optional>
std::optional<int> findValue(const std::string& key) {
if (exists(key)) {
return getValue(key);
}
return std::nullopt; // No value
}
auto result = findValue("mykey");
if (result.has_value()) {
std::cout << *result << "\n";
}
// Or use value_or for default
int val = result.value_or(0);
// Direct access (throws if empty)
try {
int v = result.value();
} catch (const std::bad_optional_access&) {
// Handle empty optional
}
std::variant
#include <variant>
// Type-safe union
std::variant<int, double, std::string> v;
v = 42;
v = 3.14;
v = "hello";
// Access with std::get
v = 42;
int i = std::get<int>(v); // OK
// double d = std::get<double>(v); // Throws std::bad_variant_access
// Safe access with std::get_if
if (auto* p = std::get_if<int>(&v)) {
std::cout << *p << "\n";
}
// Visit pattern
std::visit([](auto&& arg) {
std::cout << arg << "\n";
}, v);
// Overload pattern for different types
auto visitor = [](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << "\n";
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << arg << "\n";
}
};
std::string_view
#include <string_view>
// Non-owning view of string data
void process(std::string_view sv) {
// No allocation, just pointer + length
std::cout << sv << "\n";
}
std::string s = "hello world";
process(s); // From std::string
process("literal"); // From literal
process(s.substr(0, 5)); // From substring
// Create subviews without allocation
std::string_view view = "hello world";
std::string_view sub = view.substr(0, 5); // "hello"
if constexpr
template<typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // Dereference if pointer
} else {
return t; // Return directly otherwise
}
}
int x = 42;
int* p = &x;
getValue(x); // Returns 42
getValue(p); // Returns 42 (dereferenced)
Fold Expressions
// Sum all arguments
template<typename... Args>
auto sum(Args... args) {
return (... + args); // ((arg1 + arg2) + arg3) + ...
}
sum(1, 2, 3, 4); // 10
// Print all arguments
template<typename... Args>
void print(Args... args) {
(std::cout << ... << args) << "\n";
}
// Check if all are true
template<typename... Args>
bool allTrue(Args... args) {
return (... && args);
}
std::filesystem
#include <filesystem>
namespace fs = std::filesystem;
// Path operations
fs::path p = "/home/user/documents/file.txt";
std::cout << p.filename() << "\n"; // "file.txt"
std::cout << p.extension() << "\n"; // ".txt"
std::cout << p.parent_path() << "\n"; // "/home/user/documents"
// Check existence
if (fs::exists(p)) {
std::cout << "File exists\n";
}
// Directory iteration
for (const auto& entry : fs::directory_iterator("/path")) {
std::cout << entry.path() << "\n";
}
// Recursive iteration
for (const auto& entry : fs::recursive_directory_iterator("/path")) {
if (entry.is_regular_file()) {
std::cout << entry.path() << ": " << entry.file_size() << " bytes\n";
}
}
// Create directories
fs::create_directories("/path/to/new/dir");
// Copy, rename, remove
fs::copy("source.txt", "dest.txt");
fs::rename("old.txt", "new.txt");
fs::remove("file.txt");
fs::remove_all("directory"); // Recursive
Parallel Algorithms
#include <algorithm>
#include <execution>
#include <vector>
std::vector<int> data(1000000);
// Sequential (default)
std::sort(data.begin(), data.end());
// Parallel
std::sort(std::execution::par, data.begin(), data.end());
// Parallel and vectorized
std::sort(std::execution::par_unseq, data.begin(), data.end());
// Works with many algorithms
std::for_each(std::execution::par, data.begin(), data.end(),
[](int& x) { x *= 2; });
auto result = std::reduce(std::execution::par, data.begin(), data.end());
C++20: Major Language Update
Concepts
#include <concepts>
// Define requirements for types
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
template<typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::same_as<std::ostream&>;
};
// Use concepts to constrain templates
template<Numeric T>
T add(T a, T b) {
return a + b;
}
// Shorthand syntax
void print(Printable auto const& value) {
std::cout << value << "\n";
}
// requires clause
template<typename T>
requires std::integral<T>
T factorial(T n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
Ranges
#include <ranges>
#include <vector>
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Views are lazy and composable
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; }) // Even numbers
| std::views::transform([](int n) { return n * n; }); // Square them
for (int n : result) {
std::cout << n << " "; // 4 16 36 64 100
}
// Common views
auto first5 = nums | std::views::take(5);
auto skip3 = nums | std::views::drop(3);
auto reversed = nums | std::views::reverse;
auto indexed = std::views::iota(0, 10); // 0, 1, 2, ..., 9
// Range algorithms
std::ranges::sort(nums);
auto it = std::ranges::find(nums, 5);
bool hasEven = std::ranges::any_of(nums, [](int n) { return n % 2 == 0; });
Coroutines
#include <coroutine>
#include <generator> // C++23 or use library
// Generator coroutine
std::generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
// Usage
for (int i : range(0, 10)) {
std::cout << i << " ";
}
// Async coroutine (simplified)
Task<int> asyncCompute() {
auto result = co_await asyncOperation();
co_return result * 2;
}
Modules
// math.ixx (module interface)
export module math;
export int add(int a, int b) {
return a + b;
}
export class Calculator {
public:
int multiply(int a, int b) { return a * b; }
};
// main.cpp
import math;
int main() {
int sum = add(3, 4);
Calculator calc;
int product = calc.multiply(3, 4);
}
Three-Way Comparison (Spaceship Operator)
#include <compare>
struct Version {
int major, minor, patch;
auto operator<=>(const Version&) const = default;
};
Version v1{1, 2, 3};
Version v2{1, 3, 0};
if (v1 < v2) { } // true
if (v1 == v2) { } // false
if (v1 != v2) { } // true
// All comparison operators work!
consteval and constinit
// consteval: MUST be evaluated at compile time
consteval int compileTimeSquare(int x) {
return x * x;
}
constexpr int a = compileTimeSquare(5); // OK
// int b = compileTimeSquare(runtime_value); // ERROR
// constinit: initialized at compile time, but not const
constinit int global = 42; // Prevents static init order fiasco
// global = 100; // OK: can be modified at runtime
std::format
#include <format>
std::string s = std::format("Hello, {}!", "World");
std::string nums = std::format("{} + {} = {}", 1, 2, 3);
// Formatting options
std::format("{:>10}", 42); // " 42" (right align)
std::format("{:<10}", 42); // "42 " (left align)
std::format("{:^10}", 42); // " 42 " (center)
std::format("{:06}", 42); // "000042" (zero-padded)
std::format("{:.2f}", 3.14159); // "3.14" (precision)
std::format("{:x}", 255); // "ff" (hex)
std::format("{:b}", 42); // "101010" (binary)
// Print directly
std::print("Hello, {}!\n", "World"); // C++23
C++23: Latest Additions
std::expected
#include <expected>
std::expected<int, std::string> divide(int a, int b) {
if (b == 0) {
return std::unexpected("Division by zero");
}
return a / b;
}
auto result = divide(10, 2);
if (result.has_value()) {
std::cout << *result << "\n"; // 5
} else {
std::cout << "Error: " << result.error() << "\n";
}
// Monadic operations
auto doubled = divide(10, 2)
.transform([](int x) { return x * 2; })
.value_or(0);
std::print
#include <print>
std::print("Hello, {}!\n", "World");
std::println("Value: {}", 42); // Adds newline automatically
// To other streams
std::print(std::cerr, "Error: {}\n", message);
Deducing this
class Builder {
std::string data;
public:
// Single implementation for both lvalue and rvalue
template<typename Self>
auto&& add(this Self&& self, std::string_view s) {
self.data += s;
return std::forward<Self>(self);
}
};
Builder b;
b.add("hello").add(" ").add("world"); // Chaining works
Builder().add("temp").add("chain"); // Move semantics preserved
std::mdspan
#include <mdspan>
// Multidimensional view over contiguous data
std::vector<int> data(12);
std::mdspan<int, std::extents<size_t, 3, 4>> matrix(data.data());
matrix[1, 2] = 42; // 2D indexing
Feature Detection Macros
// Check for feature availability
#if __cpp_concepts >= 201907L
// Use concepts
#endif
#if __cpp_ranges >= 201911L
// Use ranges
#endif
// Version check
#if __cplusplus >= 202002L
// C++20 or later
#elif __cplusplus >= 201703L
// C++17
#elif __cplusplus >= 201402L
// C++14
#elif __cplusplus >= 201103L
// C++11
#endif
Exercises
Lambda Practice
- Write a function that returns a counter lambda (captures and increments)
- Write a generic lambda that prints any container
std::optional Usage
- Create a function that searches a vector and returns optional
- Chain optional operations with value_or
Ranges Pipeline
- Create a pipeline that filters, transforms, and takes elements
- Compare readability with traditional loop approach
Concepts
- Define a concept for "Container" (has begin(), end(), size())
- Write a generic print function constrained by the concept
Previous: 04 - Object-Oriented Programming Next: 06 - STL Deep Dive