Modern C++ Features (C++11/14/17/20/23)
Table of Contents
- Auto and Type Inference
- Smart Pointers
- Move Semantics
- Lambda Expressions
- Structured Bindings
- Optional, Variant, Any
- String View
- Constexpr and Compile-Time
- Ranges (C++20)
- Concepts (C++20)
- Coroutines (C++20)
- Modules (C++20)
- std::format (C++20)
- std::span (C++20)
- std::expected (C++23)
- Three-Way Comparison (C++20)
- Additional C++23 Features
Auto and Type Inference
auto
auto x = 42; // int
auto y = 3.14; // double
auto s = "hello"s; // string (with s suffix)
auto v = vector<int>{1,2,3};
// With functions
auto add(int a, int b) -> int {
return a + b;
}
// Return type deduction (C++14)
auto multiply(int a, int b) {
return a * b;
}
decltype
int x = 10;
decltype(x) y = 20; // int
// Get return type
decltype(add(1, 2)) result;
// Common with templates
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
return a + b;
}
Smart Pointers
unique_ptr
#include <memory>
// Exclusive ownership
unique_ptr<int> p1 = make_unique<int>(42);
cout << *p1 << endl;
// Transfer ownership
unique_ptr<int> p2 = move(p1);
// p1 is now nullptr
// Custom deleter
unique_ptr<FILE, decltype(&fclose)> file(fopen("f.txt", "r"), fclose);
// Array
unique_ptr<int[]> arr = make_unique<int[]>(10);
shared_ptr
// Shared ownership
shared_ptr<int> p1 = make_shared<int>(42);
shared_ptr<int> p2 = p1; // Both own
cout << p1.use_count() << endl; // 2
// Automatically deleted when last owner destroyed
weak_ptr
shared_ptr<int> shared = make_shared<int>(42);
weak_ptr<int> weak = shared;
// Check and access
if (auto locked = weak.lock()) {
cout << *locked << endl;
}
// Check if still valid
if (!weak.expired()) {
// Still valid
}
Move Semantics
Rvalue References
string s = "Hello";
string&& rref = move(s); // s is now moved-from
// Move constructor
class MyClass {
string data;
public:
MyClass(MyClass&& other) noexcept
: data(move(other.data)) {}
MyClass& operator=(MyClass&& other) noexcept {
data = move(other.data);
return *this;
}
};
std::move and std::forward
// move - cast to rvalue
vector<string> v;
string s = "Hello";
v.push_back(move(s)); // Moves instead of copies
// forward - perfect forwarding
template<typename T>
void wrapper(T&& arg) {
process(forward<T>(arg));
}
Lambda Expressions
Basic Syntax
auto add = [](int a, int b) { return a + b; };
cout << add(1, 2) << endl; // 3
// With captures
int x = 10;
auto addX = [x](int a) { return a + x; };
auto addXRef = [&x](int a) { return a + x; };
// Capture all
auto all = [=]() { }; // By value
auto allRef = [&]() { }; // By reference
Advanced Lambdas
// Mutable lambda
int count = 0;
auto counter = [count]() mutable { return ++count; };
// Generic lambda (C++14)
auto print = [](auto x) { cout << x << endl; };
// Init capture (C++14)
auto p = [ptr = make_unique<int>(42)]() { return *ptr; };
// Constexpr lambda (C++17)
auto square = [](int n) constexpr { return n * n; };
Structured Bindings
// C++17
// Pairs and tuples
pair<int, string> p = {1, "hello"};
auto [id, name] = p;
// Arrays
int arr[] = {1, 2, 3};
auto [a, b, c] = arr;
// Structs
struct Point { int x, y; };
Point pt = {10, 20};
auto [x, y] = pt;
// Maps
map<string, int> m = {{"a", 1}, {"b", 2}};
for (const auto& [key, value] : m) {
cout << key << ": " << value << endl;
}
Optional, Variant, Any
optional
#include <optional>
optional<int> findValue(bool found) {
if (found) return 42;
return nullopt;
}
auto result = findValue(true);
if (result) {
cout << *result << endl;
}
// value_or
cout << result.value_or(-1) << endl;
variant
#include <variant>
variant<int, string, double> v;
v = 42;
v = "hello";
v = 3.14;
// Access
cout << get<double>(v) << endl;
// Visit
visit([](auto&& arg) { cout << arg << endl; }, v);
any
#include <any>
any a = 42;
a = string("hello");
a = 3.14;
if (a.type() == typeid(double)) {
cout << any_cast<double>(a) << endl;
}
String View
#include <string_view>
// Non-owning view of string
string_view sv = "Hello, World!";
// Efficient substring
string_view sub = sv.substr(0, 5); // "Hello"
// From string
string s = "Test";
string_view sv2 = s;
// Function parameter
void print(string_view sv) {
cout << sv << endl;
}
print("literal"); // No allocation
print(s); // No copy
Constexpr and Compile-Time
constexpr
// Compile-time constant
constexpr int square(int n) {
return n * n;
}
constexpr int val = square(5); // Computed at compile time
// constexpr if (C++17)
template<typename T>
auto process(T value) {
if constexpr (is_integral_v<T>) {
return value * 2;
} else {
return value;
}
}
consteval (C++20)
consteval int mustBeCompileTime(int n) {
return n * n;
}
constexpr int x = mustBeCompileTime(5); // OK
// int y = mustBeCompileTime(runtime); // ERROR
Ranges (C++20)
#include <ranges>
vector<int> v = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Pipe syntax
auto result = v
| views::filter([](int n) { return n % 2 == 0; })
| views::transform([](int n) { return n * n; });
for (int x : result) {
cout << x << " "; // 4 16 36 64 100
}
// views
views::iota(1, 10) // 1-9
views::take(v, 3) // First 3
views::drop(v, 2) // Skip 2
views::reverse(v) // Reversed
views::keys(map) // Map keys
views::values(map) // Map values
Ranges Visual Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ RANGES PIPELINE VISUALIZATION │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Input: {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ views::filter │ Keep only even numbers │
│ │ (n % 2 == 0) │ │
│ └────────────────────┘ │
│ │ │
│ ▼ │
│ {2, 4, 6, 8, 10} │
│ │ │
│ ▼ │
│ ┌────────────────────┐ │
│ │ views::transform │ Square each number │
│ │ (n * n) │ │
│ └────────────────────┘ │
│ │ │
│ ▼ │
│ Output: {4, 16, 36, 64, 100} │
│ │
├─────────────────────────────────────────────────────────────────────────┤
│ KEY: Views are lazy - no intermediate containers created! │
└─────────────────────────────────────────────────────────────────────────┘
Common Range Adaptors
// take - first N elements
views::take(v, 3) // {1, 2, 3}
// drop - skip first N elements
views::drop(v, 3) // {4, 5, 6, 7, 8, 9, 10}
// take_while / drop_while
views::take_while(v, [](int n) { return n < 5; }) // {1, 2, 3, 4}
// filter - keep elements matching predicate
views::filter(v, [](int n) { return n > 5; }) // {6, 7, 8, 9, 10}
// transform - apply function to each element
views::transform(v, [](int n) { return n * 2; }) // {2, 4, 6, ...}
// reverse - reverse order
views::reverse(v) // {10, 9, 8, 7, 6, 5, 4, 3, 2, 1}
// split - split by delimiter
views::split(str, ' ') // Split string by spaces
// join - flatten nested ranges
views::join(nested_vec) // Flatten 2D to 1D
// zip (C++23) - combine multiple ranges
views::zip(v1, v2) // Pairs of elements
// enumerate (C++23) - add index
views::enumerate(v) // (0, elem0), (1, elem1), ...
Range Algorithms
#include <ranges>
#include <algorithm>
vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// Range-based algorithms (take range directly)
ranges::sort(v);
ranges::find(v, 5);
ranges::count(v, 1);
ranges::copy(v, output.begin());
// With projections
struct Person { string name; int age; };
vector<Person> people;
// Sort by age using projection
ranges::sort(people, {}, &Person::age);
// Find by name
auto it = ranges::find(people, "Alice", &Person::name);
Concepts (C++20)
Concepts provide a way to constrain template parameters with readable, named requirements.
┌─────────────────────────────────────────────────────────────────────────┐
│ CONCEPTS OVERVIEW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Template │ ───► │ Concept │ ───► │ Better │ │
│ │ Error Msg │ │ Constraint │ │ Diagnostics│ │
│ │ (Cryptic) │ │ (Named) │ │ (Clear) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Before C++20: │
│ error: no match for 'operator<<' ... │
│ ... 50 lines of template instantiation trace ... │
│ │
│ With Concepts: │
│ error: constraint 'Printable<T>' not satisfied │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Defining Concepts
#include <concepts>
// Basic concept definition
template<typename T>
concept Numeric = std::is_arithmetic_v<T>;
// Concept with requirements
template<typename T>
concept Printable = requires(T t) {
{ std::cout << t } -> std::same_as<std::ostream&>;
};
// Concept with multiple requirements
template<typename T>
concept Container = requires(T c) {
{ c.begin() } -> std::input_or_output_iterator;
{ c.end() } -> std::input_or_output_iterator;
{ c.size() } -> std::convertible_to<std::size_t>;
typename T::value_type;
};
// Compound requirements
template<typename T>
concept Hashable = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};
Using Concepts
// Method 1: requires clause
template<typename T>
requires Numeric<T>
T add(T a, T b) { return a + b; }
// Method 2: Concept as type constraint
template<Numeric T>
T multiply(T a, T b) { return a * b; }
// Method 3: Trailing requires
template<typename T>
T divide(T a, T b) requires Numeric<T> { return a / b; }
// Method 4: Abbreviated function template
void print(Printable auto value) {
std::cout << value << std::endl;
}
Standard Library Concepts
#include <concepts>
// Type concepts
std::same_as<T, U> // T is same as U
std::derived_from<D, B> // D derives from B
std::convertible_to<From, To>// From can convert to To
// Comparison concepts
std::equality_comparable<T> // == and !=
std::totally_ordered<T> // <, <=, >, >=, ==
// Object concepts
std::movable<T> // Move constructible/assignable
std::copyable<T> // Copy + movable
std::regular<T> // Copyable + default init + equality
// Arithmetic concepts
std::integral<T> // int, long, char, etc.
std::floating_point<T> // float, double
std::signed_integral<T> // Signed integers
std::unsigned_integral<T> // Unsigned integers
// Iterator concepts
std::input_iterator<T>
std::forward_iterator<T>
std::bidirectional_iterator<T>
std::random_access_iterator<T>
std::contiguous_iterator<T>
// Range concepts
std::ranges::range<T>
std::ranges::sized_range<T>
std::ranges::input_range<T>
Concept Example: Type-Safe Container
template<typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;
template<Arithmetic T>
class NumericVector {
std::vector<T> data_;
public:
T sum() const {
return std::accumulate(data_.begin(), data_.end(), T{});
}
T average() const requires std::floating_point<T> {
return sum() / data_.size();
}
void push(T value) { data_.push_back(value); }
};
// NumericVector<int> vi; // OK
// NumericVector<double> vd; // OK
// NumericVector<string> vs; // ERROR: string is not Arithmetic
Coroutines (C++20)
Coroutines are functions that can suspend execution and resume later.
┌─────────────────────────────────────────────────────────────────────────┐
│ COROUTINE EXECUTION FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Caller Coroutine │
│ │ │ │
│ │ create() │ │
│ │────────────────────────────►│ │
│ │ │ Execute until suspend │
│ │◄────────────────────────────│ │
│ │ (suspended) │ │
│ │ │ │
│ │ resume() │ │
│ │────────────────────────────►│ │
│ │ │ Continue execution │
│ │ │ until next suspend │
│ │◄────────────────────────────│ │
│ │ (value or suspended) │ │
│ │ │ │
│ │ resume() │ │
│ │────────────────────────────►│ │
│ │ │ Final execution │
│ │◄────────────────────────────│ │
│ │ (done) │ │
│ ▼ ▼ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Generator Example
#include <coroutine>
#include <iostream>
// Simple generator that yields values
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
// Iterator interface
struct iterator {
std::coroutine_handle<promise_type> handle;
iterator& operator++() {
handle.resume();
return *this;
}
T operator*() const { return handle.promise().current_value; }
bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
};
iterator begin() {
handle.resume();
return {handle};
}
std::default_sentinel_t end() { return {}; }
};
// Usage
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i; // Suspend and yield value
}
}
int main() {
for (int x : range(0, 5)) {
std::cout << x << " "; // 0 1 2 3 4
}
}
Coroutine Keywords
co_yield expr; // Suspend and yield a value
co_return expr; // Complete with a value
co_await expr; // Suspend until operation completes
Modules (C++20)
Modules replace the traditional header/source separation with a more efficient compilation model.
┌─────────────────────────────────────────────────────────────────────────┐
│ HEADERS vs MODULES COMPARISON │
├────────────────────────────────┬────────────────────────────────────────┤
│ HEADERS │ MODULES │
├────────────────────────────────┼────────────────────────────────────────┤
│ │ │
│ // header.h │ // math.ixx (module interface) │
│ #pragma once │ export module math; │
│ #include <vector> │ │
│ class MyClass { ... }; │ export int add(int a, int b) { │
│ │ return a + b; │
│ // Parsed every #include │ } │
│ // Macros leak across files │ │
│ // Slow compilation │ // Compiled once, imported quickly │
│ │ // No macro leakage │
│ // source.cpp │ // Much faster compilation │
│ #include "header.h" │ │
│ // ... uses MyClass │ // main.cpp │
│ │ import math; │
│ │ int x = add(1, 2); │
│ │ │
└────────────────────────────────┴────────────────────────────────────────┘
Module Interface Unit
// math.ixx (or math.cppm)
export module math;
// Import standard library (C++23 style)
import <iostream>;
// Private (not exported)
int internal_helper(int x) {
return x * 2;
}
// Public (exported)
export int add(int a, int b) {
return a + b;
}
export int multiply(int a, int b) {
return a * b;
}
export class Calculator {
public:
int compute(int a, int b) {
return internal_helper(a) + b;
}
};
Module Implementation Unit
// math_impl.cpp
module math; // No 'export' - this is implementation
// Can access non-exported symbols
int optimized_compute(int a, int b) {
return internal_helper(a + b);
}
Using Modules
// main.cpp
import math;
import <iostream>;
int main() {
std::cout << add(1, 2) << std::endl; // 3
std::cout << multiply(3, 4) << std::endl; // 12
Calculator calc;
std::cout << calc.compute(5, 6) << std::endl;
}
Compilation
# Compile module (creates .pcm or .ifc)
g++ -std=c++20 -fmodules-ts -c math.ixx -o math.pcm
# Compile and link with module
g++ -std=c++20 -fmodules-ts main.cpp math.pcm -o main
std::format (C++20)
Type-safe, extensible formatting library replacing printf and stringstream.
#include <format>
#include <iostream>
// Basic formatting
std::string s1 = std::format("Hello, {}!", "World");
std::string s2 = std::format("{} + {} = {}", 1, 2, 3);
// Positional arguments
std::string s3 = std::format("{1} before {0}", "second", "first");
// "first before second"
// Format specifiers
std::format("{:d}", 42); // "42" (decimal)
std::format("{:x}", 255); // "ff" (hex)
std::format("{:X}", 255); // "FF" (uppercase hex)
std::format("{:b}", 5); // "101" (binary)
std::format("{:o}", 8); // "10" (octal)
// Width and alignment
std::format("{:10}", "hi"); // "hi " (left, width 10)
std::format("{:>10}", "hi"); // " hi" (right align)
std::format("{:^10}", "hi"); // " hi " (center)
std::format("{:*^10}", "hi"); // "****hi****" (fill with *)
// Floating point
std::format("{:.2f}", 3.14159); // "3.14" (2 decimals)
std::format("{:e}", 1234.5); // "1.234500e+03" (scientific)
std::format("{:g}", 1234.5); // "1234.5" (general)
Format Specifier Syntax
┌─────────────────────────────────────────────────────────────────────────┐
│ FORMAT SPECIFIER SYNTAX │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ {:[[fill]align][sign][#][0][width][.precision][type]} │
│ │
│ Examples: │
│ {:>10} Right align, width 10 │
│ {:0>10} Right align, pad with zeros │
│ {:.3f} 3 decimal places, fixed-point │
│ {:+.2f} Always show sign, 2 decimals │
│ {:#x} Hex with 0x prefix │
│ {:*^20} Center, width 20, fill with * │
│ │
│ align: < (left) > (right) ^ (center) │
│ sign: + (always) - (negative only) space (space for positive) │
│ type: d b o x X (integers) e E f F g G (floats) s (string) │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Custom Formatter
struct Point {
int x, y;
};
template<>
struct std::formatter<Point> {
constexpr auto parse(std::format_parse_context& ctx) {
return ctx.begin();
}
auto format(const Point& p, std::format_context& ctx) const {
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
}
};
// Usage
Point p{10, 20};
std::cout << std::format("Point: {}", p); // "Point: (10, 20)"
std::print (C++23)
#include <print>
std::print("Hello, {}!\n", "World");
std::println("Line with newline: {}", 42);
std::span (C++20)
A non-owning view over contiguous memory (like string_view for arrays).
┌─────────────────────────────────────────────────────────────────────────┐
│ std::span CONCEPT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Original Array: [10][20][30][40][50] │
│ ▲ ▲ │
│ │ │ │
│ data() data()+size() │
│ │
│ span<int>: ┌──────────┐ │
│ │ data_ ───┼──► [10][20][30][40][50] │
│ │ size_: 5 │ │
│ └──────────┘ │
│ │
│ subspan: ┌──────────┐ │
│ │ data_ ───┼──────► [20][30][40] │
│ │ size_: 3 │ │
│ └──────────┘ │
│ │
│ ✓ No copying ✓ Safe bounds checking │
│ ✓ Works with arrays, vectors, C arrays │
│ │
└─────────────────────────────────────────────────────────────────────────┘
#include <span>
#include <vector>
#include <array>
// Function that works with any contiguous container
void process(std::span<int> data) {
for (int& x : data) {
x *= 2;
}
}
int main() {
// Works with C arrays
int arr[] = {1, 2, 3, 4, 5};
process(arr);
// Works with vectors
std::vector<int> vec = {1, 2, 3, 4, 5};
process(vec);
// Works with std::array
std::array<int, 5> sarr = {1, 2, 3, 4, 5};
process(sarr);
// Create span explicitly
std::span<int> sp(arr);
std::span<int> sp2(vec.data(), vec.size());
}
Span Operations
std::span<int> s(vec);
s.size(); // Number of elements
s.size_bytes(); // Size in bytes
s.empty(); // Check if empty
s.data(); // Pointer to first element
s.front(); // First element
s.back(); // Last element
s[2]; // Element access
s.first<3>(); // First 3 elements (static extent)
s.first(3); // First 3 elements (dynamic extent)
s.last<2>(); // Last 2 elements
s.last(2);
s.subspan<1, 3>(); // Elements 1-3 (static)
s.subspan(1, 3); // Elements 1-3 (dynamic)
Fixed-Size Span
// Static extent - size known at compile time
std::span<int, 5> fixed_span(arr);
// Dynamic extent - size determined at runtime
std::span<int> dynamic_span(vec);
// Function with fixed size requirement
void process_three(std::span<int, 3> data) {
// Guaranteed 3 elements
}
int arr3[3] = {1, 2, 3};
process_three(arr3); // OK
// process_three(arr); // ERROR: arr has 5 elements
std::expected (C++23)
A type for returning either a value or an error (better than exceptions for expected errors).
┌─────────────────────────────────────────────────────────────────────────┐
│ std::expected CONCEPT │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ expected<T, E> - Contains either T (value) or E (error) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ has_value() │ │ !has_value() │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ Success │ │ Error │ │
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │ value T │ │ │ │ error E │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Comparison with alternatives: │
│ │
│ optional<T> : Value or nothing (no error info) │
│ variant<T, E> : Type-safe union (less ergonomic) │
│ Exceptions : For exceptional cases, not expected errors │
│ expected<T, E> : Value OR error with full error details ✓ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
#include <expected>
#include <string>
enum class ParseError {
InvalidFormat,
OutOfRange,
Empty
};
std::expected<int, ParseError> parse_int(const std::string& s) {
if (s.empty()) {
return std::unexpected(ParseError::Empty);
}
try {
int value = std::stoi(s);
return value; // Implicit conversion to expected
} catch (const std::invalid_argument&) {
return std::unexpected(ParseError::InvalidFormat);
} catch (const std::out_of_range&) {
return std::unexpected(ParseError::OutOfRange);
}
}
// Usage
auto result = parse_int("42");
if (result) { // or result.has_value()
std::cout << "Value: " << *result << std::endl;
} else {
switch (result.error()) {
case ParseError::InvalidFormat:
std::cout << "Invalid format" << std::endl;
break;
case ParseError::OutOfRange:
std::cout << "Out of range" << std::endl;
break;
case ParseError::Empty:
std::cout << "Empty string" << std::endl;
break;
}
}
Expected Operations
std::expected<int, std::string> exp = 42;
exp.has_value(); // true
exp.value(); // 42 (throws if error)
*exp; // 42 (undefined if error)
exp.value_or(0); // 42 (or 0 if error)
exp.error(); // Error value (undefined if has value)
// Monadic operations (C++23)
exp.and_then([](int x) -> std::expected<int, std::string> {
return x * 2;
});
exp.transform([](int x) { return x * 2; });
exp.or_else([](const std::string& err) -> std::expected<int, std::string> {
std::cout << "Error: " << err << std::endl;
return 0; // Default value
});
Chaining Expected Operations
std::expected<std::string, Error> read_file(const std::string& path);
std::expected<Config, Error> parse_config(const std::string& content);
std::expected<App, Error> create_app(const Config& config);
// Chain operations (C++23)
auto result = read_file("config.json")
.and_then(parse_config)
.and_then(create_app);
if (result) {
result->run();
} else {
handle_error(result.error());
}
Three-Way Comparison (C++20)
The spaceship operator <=> provides all comparison operators in one.
┌─────────────────────────────────────────────────────────────────────────┐
│ THREE-WAY COMPARISON (SPACESHIP) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ a <=> b returns one of: │
│ │
│ ┌─────────────────┬─────────────────┬─────────────────┐ │
│ │ strong_ordering │ weak_ordering │ partial_ordering│ │
│ ├─────────────────┼─────────────────┼─────────────────┤ │
│ │ • less │ • less │ • less │ │
│ │ • equal │ • equivalent │ • equivalent │ │
│ │ • greater │ • greater │ • greater │ │
│ │ │ │ • unordered │ │
│ ├─────────────────┼─────────────────┼─────────────────┤ │
│ │ int, char, │ Case-insensitive│ floating-point │ │
│ │ pointers │ strings │ (NaN issues) │ │
│ └─────────────────┴─────────────────┴─────────────────┘ │
│ │
│ One <=> definition generates: ==, !=, <, <=, >, >= │
│ │
└─────────────────────────────────────────────────────────────────────────┘
#include <compare>
struct Point {
int x, y;
// Default spaceship operator
auto operator<=>(const Point&) const = default;
};
Point p1{1, 2}, p2{1, 3};
bool less = (p1 < p2); // true (compares x, then y)
bool equal = (p1 == p2); // false
// Custom spaceship
struct Version {
int major, minor, patch;
std::strong_ordering operator<=>(const Version& other) const {
if (auto cmp = major <=> other.major; cmp != 0) return cmp;
if (auto cmp = minor <=> other.minor; cmp != 0) return cmp;
return patch <=> other.patch;
}
bool operator==(const Version&) const = default;
};
Version v1{1, 2, 3}, v2{1, 3, 0};
bool newer = (v2 > v1); // true
Additional C++23 Features
if consteval
constexpr int compute(int x) {
if consteval {
// Compile-time only code
return x * 2; // Can use consteval functions
} else {
// Runtime code
return x * 2; // Can use non-constexpr operations
}
}
Deducing this (Explicit Object Parameter)
struct Widget {
// Single implementation for both const and non-const
template<typename Self>
auto& value(this Self&& self) {
return self.value_;
}
// CRTP without inheritance
void print(this auto&& self) {
std::cout << self.name() << std::endl;
}
private:
int value_;
};
std::mdspan (Multidimensional Span)
#include <mdspan>
std::vector<int> data(12);
std::iota(data.begin(), data.end(), 0);
// View as 3x4 matrix
std::mdspan matrix(data.data(), 3, 4);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << matrix[i, j] << " ";
}
std::cout << "\n";
}
// 0 1 2 3
// 4 5 6 7
// 8 9 10 11
std::stacktrace
#include <stacktrace>
void problematic_function() {
auto trace = std::stacktrace::current();
std::cout << std::to_string(trace);
}
Lambda Improvements
// Attributes on lambdas
auto f = []() [[nodiscard]] { return 42; };
// static operator() (no captures, more efficient)
auto add = [](int a, int b) static { return a + b; };
Quick Reference
┌─────────────────────────────────────────────────────────────────────────┐
│ MODERN C++ QUICK REFERENCE │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ C++11/14: C++17: │
│ ───────── ────── │
│ auto, decltype structured bindings │
│ unique_ptr, shared_ptr optional, variant, any │
│ move semantics string_view │
│ lambdas if constexpr │
│ range-based for fold expressions │
│ │
│ C++20: C++23: │
│ ────── ────── │
│ concepts std::expected │
│ ranges std::print/println │
│ coroutines deducing this │
│ modules std::mdspan │
│ std::format std::stacktrace │
│ std::span if consteval │
│ three-way comparison (<=>) ranges::zip, enumerate │
│ consteval, constinit std::generator │
│ │
└─────────────────────────────────────────────────────────────────────────┘
// Type inference
auto x = expr;
decltype(expr) y;
// Smart pointers
unique_ptr<T> p = make_unique<T>(args);
shared_ptr<T> p = make_shared<T>(args);
weak_ptr<T> w = sharedPtr;
// Move semantics
T&& rref;
move(x);
forward<T>(x);
// Lambdas
[captures](params) { body }
[=] [&] [x] [&x] [x = expr]
// Structured bindings
auto [a, b] = pair;
auto& [x, y] = struct;
// Optional/Variant/Any
optional<T>, nullopt, .value_or()
variant<T1,T2>, get<T>(), visit()
any, any_cast<T>()
// string_view & span
string_view sv = str;
span<T> s = container;
// constexpr
constexpr T func();
if constexpr (cond) { }
consteval T must_be_compile_time();
// Concepts
template<Concept T> or requires Concept<T>
// Ranges
v | views::filter(pred) | views::transform(fn)
// Format
std::format("{} {}", arg1, arg2);
std::print("{}\n", value); // C++23
// Expected (C++23)
std::expected<T, E> result = func();
result.value_or(default);
result.and_then(next_func);
// Three-way comparison
auto cmp = a <=> b; // strong_ordering, weak_ordering, or partial_ordering
Compile & Run
# C++17
g++ -std=c++17 -Wall examples.cpp -o examples && ./examples
# C++20
g++ -std=c++20 -Wall examples.cpp -o examples && ./examples
# C++23 (requires GCC 13+ or Clang 17+)
g++ -std=c++23 -Wall examples.cpp -o examples && ./examples
# With modules (C++20)
g++ -std=c++20 -fmodules-ts module.cpp main.cpp -o main