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

Pick one feature and use it in your next project. Structured bindings and std::optional are an easy place to start.

← Back to all posts