March 15, 2026 • 4 min read
Modern C++ Features You Should Use
A quick tour of C++17 and C++20 features that make your code cleaner and safer.
If your picture of C++ is raw pointers, new and delete, and template error messages that scroll for pages, it's time for an update. Here are the C++17 and C++20 features we reach for every day. All of them work in current versions of GCC, Clang, and MSVC.
C++17
Structured Bindings
Unpack a pair, tuple, or struct into named variables, much like destructuring in JavaScript:
#include <iostream>
#include <map>
#include <string>
int main() {
std::map<std::string, int> ages = {{"Ada", 36}, {"Grace", 85}};
for (const auto& [name, age] : ages) {
std::cout << name << " is " << age << '\n';
}
}
No more it->first and it->second.
if With an Initializer
Declare a variable that only exists inside the if and its else:
if (auto it = ages.find("Ada"); it != ages.end()) {
std::cout << "Found Ada, age " << it->second << '\n';
}
// `it` is out of scope here, so it can't be misused later.
std::optional and std::string_view
std::optional<T> says "a T, or nothing", which beats returning -1 or a null pointer to signal failure. std::string_view is a read-only view of a string that never copies it. Together:
#include <charconv>
#include <optional>
#include <string_view>
std::optional<int> parse_port(std::string_view text) {
int port = 0;
const char* end = text.data() + text.size();
auto [ptr, error] = std::from_chars(text.data(), end, port);
if (error != std::errc{} || ptr != end || port < 1 || port > 65535) {
return std::nullopt;
}
return port;
}
The caller can't forget to check:
if (auto port = parse_port("8080")) {
std::cout << "Listening on " << *port << '\n';
}
std::cout << parse_port("oops").value_or(80) << '\n'; // prints 80
One rule for std::string_view: it doesn't own the characters, so never let it outlive the string it points into.
C++20
Concepts
Concepts put requirements on template parameters, so a mistake gives you one readable error instead of pages of noise:
#include <concepts>
template <std::integral T>
T greatest_common_divisor(T a, T b) {
while (b != 0) {
T remainder = a % b;
a = b;
b = remainder;
}
return a;
}
greatest_common_divisor(12, 18) returns 6. greatest_common_divisor(1.5, 2.0) fails to compile with an error saying double doesn't satisfy std::integral. (The standard library already has std::gcd; this one is just for show.)
For short functions there's an even lighter syntax:
auto twice(std::integral auto x) { return x * 2; }
Ranges
Ranges let you chain operations on a collection, and the views are lazy: nothing is computed until you loop over the result.
#include <iostream>
#include <ranges>
#include <vector>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even_squares = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int n : even_squares) {
std::cout << n << ' '; // 4 16 36 64 100
}
}
The algorithms also take whole containers now, so std::ranges::sort(numbers) replaces std::sort(numbers.begin(), numbers.end()).
std::span
A std::span is a view of a contiguous run of elements: a pointer and a length that travel together. One function works with vectors, arrays, and C arrays:
#include <span>
double average(std::span<const double> values) {
if (values.empty()) {
return 0.0;
}
double sum = 0.0;
for (double value : values) {
sum += value;
}
return sum / values.size();
}
std::format
Type-safe formatting with Python-style placeholders, and no more printf format bugs:
#include <format>
#include <string>
std::string line = std::format("{:<10}{:>8.2f}", "Total", 42.5);
// "Total 42.50"
C++23 adds std::print, which formats and prints in one step.
Designated Initializers
Name the fields you set, and let the rest keep their defaults:
#include <string>
struct ServerConfig {
std::string host = "localhost";
int port = 8080;
bool use_tls = false;
};
ServerConfig config{.port = 443, .use_tls = true};
The fields must appear in the order they're declared.
Three-Way Comparison
Write one line and get ==, !=, <, <=, >, and >=, all comparing member by member:
#include <compare>
struct Version {
int major;
int minor;
int patch;
auto operator<=>(const Version&) const = default;
};
static_assert(Version{1, 2, 0} < Version{1, 10, 0});
Try Them Yourself
- Paste any of these examples into Compiler Explorer and add
-std=c++20. - Test yourself with our Modern C++ quiz.
- Look up the details on cppreference.
Pick one feature and use it in your next project. Structured bindings and std::optional are an easy place to start.