C++XX: version of C++ released in 20XX
The big update that brought modern C++.
Before C++11, when you passed or returned objects by value, copies were always made. For large objects (like vectors, strings, or complex data structures), this could be very expensive.
Example:
std::vector make_vector() {
std::vector v(1000000, 42);
return v; // expensive copy before C++11
}
Even though the temporary v was about to be destroyed, C++ had to copy it into the
return value.
Move semantics lets you transfer ownership of resources (like heap memory or file handles) from one object to another, instead of copying them. A move is basically a cheap transfer of internal pointers — no deep duplication.
Before C++11, we already had lvalues and rvalues, but they became much more important.
int x = 10; // x is an lvalue →
T&.
int y = x +
5; // (x + 5) is an rvalue → T&&.
C++11 introduced std::move() to explicitly indicate that an object can be "moved from", simply
meaning that it casts its argument to an rvalue. One common use case is in move constructors, for example:
#include <iostream>
#include <string>
#include <utility> // for std::move
struct S {
std::string data;
S(const std::string& d) : data(d) { std::cout << "copy constructor\n"; }
S(std::string&& d) : data(std::move(d)) {
// Even though 'd' is an rvalue reference, it's a *named variable*, so in reality an lvalue!
// This is because every named variable has an address and can be used again, which makes
// it an lvalue expression, even if its type is an rvalue reference (T&&). We have to make
// sure to use std::move to cast it back to an rvalue so that the string's move constructor
// is called instead of the copy constructor!
std::cout << "move constructor\n";
}
};
int main() {
std::string s = "hello";
S a(s); // uses copy constructor — 's' is an lvalue
S b(std::move(s)); // uses move constructor — 's' turned into rvalue
S c("temp"); // uses move constructor — temporary is an rvalue
}
Additionally, C++11 introduced std::forward, which is a special cast used in template
functions to preserve how an argument was originally passed — whether it was an lvalue or an rvalue.
std::forward returns it as an lvalue.std::forward returns it as an rvalue.
std::forward preserves the value category (lvalue vs rvalue) of an argument when passing it
to another function. This enables perfect forwarding — efficiently forwarding arguments without
unnecessary copies. It's what makes std::make_unique, std::make_shared, and
emplace_back possible.
#include <iostream>
#include <string>
#include <utility>
struct S {
std::string data;
S(const std::string& d) : data(d) { std::cout << "copy constructor\n"; }
S(std::string&& d) : data(std::move(d)) { std::cout << "move constructor\n"; }
};
template<typename T, typename Arg>
T make_object(Arg&& arg) {
return T(std::forward<Arg>(arg)); // preserves lvalue/rvalue
}
int main() {
std::string s = "hello";
S a = make_object<S>(s); // copy constructor
S b = make_object<S>(std::move(s)); // move constructor
S c = make_object<S>("temp"); // move constructor
}
Before C++11, manual memory management with raw pointers was error-prone, leading to memory leaks and dangling pointers. Smart pointers automatically manage object lifetime using RAII (Resource Acquisition Is Initialization).
std::unique_ptr - Exclusive ownership:
#include <memory>
#include <iostream>
struct Widget {
Widget() { std::cout << "Widget created\n"; }
~Widget() { std::cout << "Widget destroyed\n"; }
};
void example() {
std::unique_ptr<Widget> ptr = std::make_unique<Widget>();
// Widget automatically destroyed when ptr goes out of scope
// Cannot copy (deleted copy constructor)
// auto ptr2 = ptr; // ERROR!
// Can move ownership
auto ptr2 = std::move(ptr); // ptr is now nullptr
}
std::shared_ptr - Shared ownership with reference counting:
void example() {
std::shared_ptr<Widget> ptr1 = std::make_shared<Widget>();
std::cout << "Count: " << ptr1.use_count() << "\n"; // 1
{
std::shared_ptr<Widget> ptr2 = ptr1; // copy allowed
std::cout << "Count: " << ptr1.use_count() << "\n"; // 2
} // ptr2 destroyed, count decreases
std::cout << "Count: " << ptr1.use_count() << "\n"; // 1
} // Widget destroyed when last shared_ptr dies
The circular reference problem:
struct Node {
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // both are shared_ptr - BAD!
~Node() { std::cout << "Node destroyed\n"; }
};
void bad_example() {
auto node1 = std::make_shared<Node>(); // node1 refcount = 1
auto node2 = std::make_shared<Node>(); // node2 refcount = 1
node1->next = node2; // node2 refcount = 2 (node1 owns it too)
node2->prev = node1; // node1 refcount = 2 (node2 owns it too)
// When function exits:
// - local node1 destroyed → node1 refcount = 1 (still owned by node2->prev)
// - local node2 destroyed → node2 refcount = 1 (still owned by node1->next)
// Both nodes keep each other alive forever! Memory leak!
// "Node destroyed" is NEVER printed
}
Each node keeps the other alive because they both have a reference count > 0. Neither can be destroyed, causing a memory leak.
std::weak_ptr - Solution: Non-owning reference that breaks cycles:
struct Node {
std::shared_ptr<Node> next; // ownership: forward direction
std::weak_ptr<Node> prev; // non-owning: backward reference
~Node() { std::cout << "Node destroyed\n"; }
};
void good_example() {
auto node1 = std::make_shared<Node>(); // node1 refcount = 1
auto node2 = std::make_shared<Node>(); // node2 refcount = 1
node1->next = node2; // node2 refcount = 2
node2->prev = node1; // node1 refcount still 1 (weak_ptr doesn't count!)
// To use weak_ptr, must convert to shared_ptr first
if (auto shared = node2->prev.lock()) {
// shared is a valid shared_ptr to node1
std::cout << "node1 is still alive\n";
}
// When function exits:
// - local node1 destroyed → node1 refcount = 0 → node1 deleted
// - local node2 destroyed → node2 refcount = 1 → decrements to 0 → node2 deleted
// "Node destroyed" printed twice - no leak!
}
weak_ptr observes but doesn't own. It must be converted to shared_ptr
via .lock() before use (returns empty shared_ptr if object was destroyed).
Before C++11, passing simple functions to algorithms required defining separate function objects or functions. Lambdas allow you to define anonymous functions inline, making code more concise and readable.
Basic syntax: [capture](parameters) -> return_type { body }
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << "\n"; // 7
int x = 10;
auto add_x = [x](int y) { return x + y; };
std::cout << add_x(5) << "\n"; // 15
int counter = 0;
auto increment = [&counter]() { counter++; };
increment();
increment();
std::cout << counter << "\n"; // 2
Capture modes:
[] - capture nothing[x] - capture x by value[&x] - capture x by reference[=] - capture all by value[&] - capture all by referenceBefore C++11, traditional enums had several problems: they implicitly converted to integers, polluted the surrounding namespace, and allowed comparisons between different enum types.
C++11 introduced enum class (also called scoped enums) to solve these issues:
enum Color { Red, Green, Blue }; // old-style enums
enum Fruit { Apple, Orange, Banana };
enum class Color { Red, Green, Blue }; // introduction of enum class
enum class Fruit { Apple, Orange, Banana };
int main() {
Color c_old = Red; // Red pollutes the enclosing namespace
int x_old = c_old; // implicit conversion to int — dangerous!
Color c_new = Color::Red; // must use scope resolution
int x_new = static_cast<int>(c_new); // explicit cast required
if (Red == Apple) {} // compiles! different enum types compared
if (Color::Red == Fruit::Apple) {} // ERROR: can't compare different enum types
}
Specifying underlying type:
enum class Status : uint8_t { // uses 1 byte instead of default int
OK = 0,
Warning = 1,
Error = 2
};
// Old enums can also specify underlying type in C++11
enum OldStyle : uint16_t { A, B, C };
Compiler automatically deduces the type:
std::vector<int> vec = {1, 2, 3};
auto it = vec.begin(); // instead of std::vector<int>::iterator
auto lambda = [](int x) { return x * 2; };
Clean syntax for iterating over containers:
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto x : vec) {} // by value (copy)
for (auto& x : vec) {} // by reference (can modify)
for (const auto& x : vec) {} // by const reference (efficient, read-only)
Type-safe null pointer that fixes ambiguity issues with NULL (represented as a 0 integer):
void func(int x) {}
void func(char* ptr) {}
func(NULL); // calls int version - wrong!
func(nullptr); // calls pointer version - correct!
int* p = nullptr; // clear and type-safe
Evaluate at compile time:
constexpr int square(int x) { return x * x; }
constexpr int size = square(5); // evaluated at compile time
int arr[size]; // size must be compile-time constant
Return type after parameters using ->; useful when it depends on arguments.
template
auto add(T a, U b) -> decltype(a + b) { return a + b; }
auto lam = [](int x) -> long { return x; }; // trailing return on lambda
Templates that accept any number of arguments:
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
print(1, 2.5, "hello", 'c'); // prints: 1 2.5 hello c
TODO: Add content about C++11 threading library (std::thread, std::mutex, std::lock_guard, std::condition_variable, std::atomic, etc.)
Small new features and improvements.
Lambda parameters can use auto to be type-generic.
auto add = [](auto a, auto b) { return a + b; };
static_assert(add(1, 2) == 3);
static_assert(add(1.5, 2.5) == 4.0);
Functions can deduce return type with auto (no trailing return needed).
auto sum(int a, int b) { return a + b; } // returns int
auto make_vec(int n) { return std::vector(n); } // returns std::vector
Factory for std::unique_ptr; safe and concise object creation.
struct W { W(int, std::string) {} };
auto p = std::make_unique(42, "ok");
constexpr functions may use local variables, loops, and branches.
constexpr int fact(int n) {
int r = 1;
for (int i = 2; i <= n; ++i) r *= i;
return r;
}
static_assert(fact(5) == 120);
Binary integer literals and apostrophe digit separators improve readability.
int flags = 0b1010'1100; // 172
auto million = 1'000'000; // 1000000
A practical release focusing on cleaner code and performance improvements.
Structured bindings allow unpacking tuples, pairs, and structs directly into named variables.
#include <tuple>
#include <string>
#include <iostream>
std::tuple<int, std::string> get_person() {
return {25, "Alice"};
}
int main() {
auto [age, name] = get_person(); // unpack directly
std::cout << name << " is " << age << " years old\n";
}
They can also be used with std::map or std::unordered_map iteration:
std::map<std::string, int> scores = {{"Alice", 90}, {"Bob", 80}};
for (auto [name, score] : scores)
std::cout << name << ": " << score << "\n";
You can now initialize a variable directly inside an if or switch statement,
limiting its scope to that statement.
if (auto it = scores.find("Bob"); it != scores.end()) {
std::cout << "Bob's score: " << it->second << "\n";
} else {
std::cout << "Bob not found\n";
}
New standard utilities to represent optional or flexible data.
Represents an optional value that may or may not exist.
#include <optional>
std::optional<int> find_user_id(std::string name) {
if (name == "Alice") return 42;
return std::nullopt;
}
int main() {
if (auto id = find_user_id("Alice"))
std::cout << "ID = " << *id << "\n";
else
std::cout << "User not found\n";
}
Type-safe union — holds one of several types at a time.
#include <variant>
std::variant<int, std::string> data;
data = 10;
std::cout << std::get<int>(data) << "\n";
data = "hello";
std::cout << std::get<std::string>(data) << "\n";
// std::get_if returns pointer or nullptr
if (auto p = std::get_if<int>(&data))
std::cout << "int: " << *p << "\n";
else
std::cout << "not an int\n";
A type-safe container for a single value of any type.
#include <any>
std::any a = 5;
std::cout << std::any_cast<int>(a) << "\n";
a = std::string("Hello");
std::cout << std::any_cast<std::string>(a) << "\n";
The new filesystem library provides portable tools for file and directory manipulation.
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;
int main() {
fs::create_directory("example");
for (auto& p : fs::directory_iterator(".")) {
std::cout << p.path() << "\n";
}
}
Simplify variadic templates by allowing concise reduction over parameter packs.
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // expands to (((arg1 + arg2) + arg3) + ...)
}
int main() {
std::cout << sum(1, 2, 3, 4) << "\n"; // 10
}
Allow defining variables in headers without violating the one-definition rule.
struct Config {
static inline const std::string name = "App";
static inline int version = 2;
};
More operations allowed in constexpr functions — including if,
switch, and most loops.
constexpr.[[nodiscard]] int compute() { return 42; }
int main() {
compute(); // compiler may warn: 'nodiscard' result ignored
}
A major modernizing release — introducing concepts, ranges, coroutines, and modules.
Concepts allow you to constrain template parameters with readable, compile-time checks.
#include <concepts>
#include <iostream>
template<std::integral T>
T add(T a, T b) { return a + b; }
int main() {
std::cout << add(3, 4) << "\n"; // OK
// add(3.5, 2.1); // compile error: not integral
}
You can also define your own concepts:
template<typename T>
concept Incrementable = requires(T x) {
++x; x++;
};
template<Incrementable T>
void increment(T& value) { ++value; }
The Ranges library introduces a powerful new way to compose algorithms.
#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
auto even_doubled =
v | std::views::filter([](int n){ return n % 2 == 0; })
| std::views::transform([](int n){ return n * 2; });
for (int n : even_doubled)
std::cout << n << " "; // 4 8
}
Ranges make algorithms composable and lazy — they only compute what’s needed.
Coroutines enable writing asynchronous or lazy computations with co_await,
co_yield, and co_return.
#include <coroutine>
#include <iostream>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle<promise_type>;
struct promise_type {
int current_value;
auto get_return_object() { return Generator{handle_type::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always yield_value(int value) { current_value = value; return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
handle_type h;
Generator(handle_type h) : h(h) {}
~Generator() { h.destroy(); }
bool next() { h.resume(); return !h.done(); }
int value() const { return h.promise().current_value; }
};
Generator counter(int n) {
for (int i = 0; i < n; ++i)
co_yield i;
}
int main() {
auto g = counter(3);
while (g.next())
std::cout << g.value() << "\n"; // 0 1 2
}
Modules are a new way to organize code — faster to compile and easier to maintain than headers.
// math.ixx
export module math;
export int add(int a, int b) { return a + b; }
// main.cpp
import math;
#include <iostream>
int main() {
std::cout << add(2, 3) << "\n";
}
C++20 allows dynamic memory, virtual calls, and most STL containers in constexpr code.
constexpr int factorial(int n) {
if (n <= 1) return 1;
else return n * factorial(n - 1);
}
static_assert(factorial(5) == 120);
Defines all comparison operators in one go.
#include <compare>
struct Point {
int x, y;
auto operator<=>(const Point&) const = default; // generates ==, <, >, etc.
};
The <chrono> library now includes calendars, time zones, and parsing/formatting.
#include <chrono>
#include <iostream>
int main() {
using namespace std::chrono;
auto today = floor<days>(system_clock::now());
std::cout << "Days since epoch: " << today.time_since_epoch().count() << "\n";
}
Point p{.x = 1, .y = 2};<=>.format().#include <format>
std::string msg = std::format("Hello, {}!", "world");
std::cout << msg; // Hello, world!
A refinement release — polishing C++20 and adding practical modern features.
std::expected represents a value or an error without throwing exceptions.
#include <expected>
#include <iostream>
#include <string>
std::expected<int, std::string> parse_number(const std::string& s) {
try {
return std::stoi(s);
} catch (...) {
return std::unexpected("Invalid number");
}
}
int main() {
auto result = parse_number("42");
if (result)
std::cout << "Parsed: " << *result << "\n";
else
std::cerr << "Error: " << result.error() << "\n";
}
A simpler, faster replacement for std::cout, inspired by Python’s print().
#include <print>
int main() {
std::print("Hello, {}!\n", "world");
std::println("Pi ≈ {:.3f}", 3.14159);
}
this
Member functions can now declare this as a parameter, improving generic and fluent APIs.
struct Point {
int x, y;
auto& move(this auto& self, int dx, int dy) {
self.x += dx;
self.y += dy;
return self; // allows chaining
}
};
int main() {
Point p{1, 2};
p.move(2, 3).move(1, 1);
std::println("({}, {})", p.x, p.y); // (4, 6)
}
C++23 extends the Ranges library with new views and algorithms.
std::views::zip — combine multiple ranges.std::views::repeat — infinite repetition of values.std::views::chunk — group elements into subranges.std::ranges::fold_left and fold_right — powerful accumulators.#include <ranges>
#include <vector>
#include <iostream>
int main() {
std::vector<int> a = {1, 2, 3};
std::vector<char> b = {'A', 'B', 'C'};
for (auto [num, letter] : std::views::zip(a, b))
std::cout << num << " - " << letter << "\n";
}
More standard library features are now constexpr, including std::string,
std::vector, and algorithms.
constexpr bool all_even() {
std::vector v = {2, 4, 6};
return std::ranges::all_of(v, [](int n){ return n % 2 == 0; });
}
static_assert(all_even());
A lightweight, multidimensional view for numerical computing.
#include <mdspan>
#include <iostream>
int main() {
double data[6] = {1, 2, 3, 4, 5, 6};
std::mdspan m(data, 2, 3); // 2 rows, 3 columns
std::cout << m(1, 2) << "\n"; // 6
}
You can now iterate multiple ranges in parallel directly in a range-based for loop.
std::vector<int> a = {1, 2, 3};
std::vector<char> b = {'x', 'y', 'z'};
for (auto [num, letter] : std::views::zip(a, b))
std::print("{}{}\n", num, letter);