Modern C++ Features Every Systems Programmer Should Know
From smart pointers to concepts and ranges - master the C++ features that make low-level programming safer and more expressive.
Introduction
C++ has evolved dramatically since C++11, introducing features that make the language safer, more expressive, and easier to use while maintaining its performance edge. This guide covers the most impactful modern C++ features every systems programmer should know.
Smart Pointers: Goodbye Manual Memory Management
Modern C++ provides three smart pointer types that automatically manage memory lifetime:
unique_ptr - Exclusive Ownership
#include <memory>
// Old way - manual memory management
void old_way() {
int* ptr = new int(42);
// ... code that might throw ...
delete ptr; // Might never execute!
}
// Modern way - automatic cleanup
void modern_way() {
auto ptr = std::make_unique<int>(42);
// Automatically deleted when scope exits
// Even if an exception is thrown!
}
shared_ptr - Shared Ownership
auto shared = std::make_shared<Resource>();
auto copy = shared; // Reference count: 2
// Resource freed when last shared_ptr is destroyed
weak_ptr - Breaking Cycles
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Prevents circular reference
};
Move Semantics: Zero-Cost Transfers
Move semantics allow transferring resources without copying, crucial for performance:
class Buffer {
std::unique_ptr<char[]> data;
size_t size;
public:
// Move constructor - steals resources
Buffer(Buffer&& other) noexcept
: data(std::move(other.data))
, size(other.size) {
other.size = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
data = std::move(other.data);
size = other.size;
other.size = 0;
return *this;
}
};
// Usage
Buffer createBuffer() {
Buffer buf(1024);
return buf; // Moved, not copied!
}
Buffer b = createBuffer(); // Zero copies
Lambda Expressions
Lambdas provide inline, anonymous functions with powerful capture semantics:
std::vector<int> nums = {1, 2, 3, 4, 5};
// Capture by value
int multiplier = 3;
std::transform(nums.begin(), nums.end(), nums.begin(),
[multiplier](int n) { return n * multiplier; });
// Capture by reference
int sum = 0;
std::for_each(nums.begin(), nums.end(),
[&sum](int n) { sum += n; });
// Generic lambda (C++14)
auto add = [](auto a, auto b) { return a + b; };
add(1, 2); // int
add(1.5, 2.5); // double
constexpr: Compile-Time Computation
Move calculations from runtime to compile time:
constexpr int factorial(int n) {
return n <= 1 ? 1 : n * factorial(n - 1);
}
// Computed at compile time!
constexpr int fact5 = factorial(5); // 120
// C++17: constexpr if
template<typename T>
auto process(T value) {
if constexpr (std::is_integral_v<T>) {
return value * 2;
} else {
return value + value;
}
}
Structured Bindings (C++17)
Decompose objects into named variables:
std::map<std::string, int> scores;
// Old way
for (const auto& pair : scores) {
std::cout << pair.first << ": " << pair.second;
}
// Modern way - structured bindings
for (const auto& [name, score] : scores) {
std::cout << name << ": " << score;
}
// Works with tuples, pairs, arrays, and custom types
auto [x, y, z] = std::make_tuple(1, 2.0, "three");
std::optional, std::variant, std::any
Type-safe alternatives to null pointers and unions:
// optional - nullable value types
std::optional<int> find_index(const std::string& s, char c) {
if (auto pos = s.find(c); pos != std::string::npos) {
return pos;
}
return std::nullopt;
}
if (auto idx = find_index("hello", 'l')) {
std::cout << "Found at: " << *idx;
}
// variant - type-safe union
std::variant<int, double, std::string> value;
value = 42;
value = 3.14;
value = "hello";
std::visit([](auto&& v) { std::cout << v; }, value);
Concepts (C++20)
Constrain templates with readable, reusable requirements:
// Define a concept
template<typename T>
concept Numeric = std::integral<T> || std::floating_point<T>;
// Use the concept
template<Numeric T>
T add(T a, T b) {
return a + b;
}
// Or with requires clause
template<typename T>
requires std::is_arithmetic_v<T>
T multiply(T a, T b) {
return a * b;
}
add(1, 2); // OK
add("a", "b"); // Compile error with clear message!
Ranges (C++20)
Composable, lazy algorithms for cleaner code:
#include <ranges>
std::vector<int> nums = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// Filter and transform with pipes
auto result = nums
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; })
| std::views::take(3);
// result: 4, 16, 36 (lazy evaluation!)
Conclusion
Modern C++ is a different language from the C++ of the 90s. These features enable
writing safer, more expressive code without sacrificing performance. Start with
smart pointers and move semantics, then gradually adopt newer features as you
become comfortable. The compiler is your ally - use -std=c++20 and
embrace the evolution.