Skip to content
Laviyx Learn

Try

C++ course menu

Modern C++ Features: auto, Structured Bindings, optional, variant, string_view, format and Ranges

A tour of the modern C++ features (C++11 to C++23) that make code shorter and safer: auto, range-based for, structured bindings, std::optional, std::variant, std::string_view, std::span, std::format, ranges, and designated initializers.

Advanced13 min readUpdated
On this page

C++ changed enormously starting with C++11, and it keeps improving every three years. Code written in modern C++ is shorter, safer and often faster than the "C with classes" style you may find in older books and answers online. This lesson collects the most useful features in one place. Many have appeared earlier in the course; here you see them side by side.

StandardHighlights
C++11auto, range-based for, lambdas, smart pointers, move semantics, nullptr, constexpr, enum class
C++14Generic lambdas, std::make_unique
C++17Structured bindings, std::optional, std::variant, std::string_view, if with initializer, <filesystem>
C++20Concepts, ranges, std::span, std::format, <=>, designated initializers, contains
C++23std::print, std::expected, std::ranges::to

auto and range-based for#

C++
#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock = {{"pens", 40}, {"books", 12}};

    // Before: std::map<std::string, int>::const_iterator it = stock.begin();
    for (auto it = stock.begin(); it != stock.end(); ++it) {
        std::cout << it->first << ' ';
    }
    std::cout << '\n';

    for (const auto& [item, count] : stock) {   // structured binding
        std::cout << item << '=' << count << ' ';
    }
    std::cout << '\n';
}
Output
books pens
books=12 pens=40

std::optional: a value that might be missing#

std::optional<T> (C++17) holds either a T or nothing. It replaces magic values like -1 or empty strings for "not found":

C++
#include <iostream>
#include <optional>
#include <string>
#include <vector>

std::optional<int> find_index(const std::vector<std::string>& v, const std::string& target) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (v[i] == target) return static_cast<int>(i);
    }
    return std::nullopt;   // nothing found
}

int main() {
    std::vector<std::string> names = {"Asha", "Ravi", "Meera"};

    if (auto idx = find_index(names, "Ravi")) {
        std::cout << "Ravi at " << *idx << '\n';
    }
    auto missing = find_index(names, "John");
    std::cout << std::boolalpha << missing.has_value() << ' ' << missing.value_or(-1) << '\n';
}
Output
Ravi at 1
false -1

The function's return type now documents that it can fail, and the caller cannot forget to check.

std::variant: one of several types#

std::variant<A, B, C> holds exactly one value of one of the listed types, a type-safe union. std::visit runs the right code for whichever type is stored:

C++
#include <iostream>
#include <string>
#include <variant>
#include <vector>

struct Circle { double r; };
struct Square { double side; };

using Shape = std::variant<Circle, Square>;

double area(const Shape& s) {
    return std::visit([](const auto& shape) -> double {
        using T = std::decay_t<decltype(shape)>;
        if constexpr (std::is_same_v<T, Circle>) {
            return 3.14159 * shape.r * shape.r;
        } else {
            return shape.side * shape.side;
        }
    }, s);
}

int main() {
    std::vector<Shape> shapes = {Circle{1}, Square{3}};
    for (const auto& s : shapes) std::cout << area(s) << '\n';

    std::variant<int, std::string> setting = "dark";
    std::cout << std::boolalpha << std::holds_alternative<std::string>(setting) << '\n';
}
Output
3.14159
9
true

Variants are an alternative to inheritance when the set of types is fixed and known: no heap allocation and no virtual functions.

std::string_view: read-only strings without copying#

std::string_view (C++17) is a lightweight view of characters owned by someone else: a string, a literal, or part of either. Taking it as a parameter avoids creating std::string copies:

C++
#include <iostream>
#include <string>
#include <string_view>

bool is_image(std::string_view filename) {
    return filename.ends_with(".png") || filename.ends_with(".jpg");
}

int main() {
    std::string upload = "holiday.jpg";
    std::cout << std::boolalpha << is_image(upload) << ' ' << is_image("notes.txt") << '\n';

    std::string_view text = "key=value";
    auto eq = text.find('=');
    std::cout << text.substr(0, eq) << " / " << text.substr(eq + 1) << '\n';   // no allocations
}
Output
true false
key / value

std::span: a view of contiguous elements#

std::span<T> (C++20) is the array equivalent of string_view: one parameter type that accepts a vector, an std::array or a C array, with its size:

C++
#include <array>
#include <iostream>
#include <span>
#include <vector>

double average(std::span<const double> values) {
    double sum = 0;
    for (double v : values) sum += v;
    return values.empty() ? 0 : sum / values.size();
}

int main() {
    std::vector<double> v = {2, 4, 6};
    std::array<double, 2> a = {10, 20};
    double c[] = {1, 2, 3, 4};

    std::cout << average(v) << ' ' << average(a) << ' ' << average(c) << '\n';
}
Output
4 15 2.5

std::format: Python-style formatting#

std::format (C++20, from <format>) builds strings with {} placeholders, much more readable than chains of << and manipulators:

C++
#include <format>
#include <iostream>
#include <string>

int main() {
    std::string name = "Asha";
    double balance = 2500.756;

    std::cout << std::format("{} has Rs {:.2f}\n", name, balance);
    std::cout << std::format("|{:<8}|{:>8}|{:^8}|\n", "left", "right", "mid");
    std::cout << std::format("{:05d} {:x} {:b}\n", 42, 255, 5);
}
Output
Asha has Rs 2500.76
|left    |   right|  mid   |
00042 ff 101

C++23 adds std::print and std::println (from <print>), which format and print in one call.

Ranges and views#

C++20 ranges let algorithms take whole containers and compose lazily with |:

C++
#include <algorithm>
#include <iostream>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> nums = {7, 2, 9, 4, 6, 1};

    std::ranges::sort(nums);
    auto it = std::ranges::find(nums, 6);
    std::cout << "6 at index " << (it - nums.begin()) << '\n';

    for (int x : nums | std::views::reverse | std::views::take(3)) {
        std::cout << x << ' ';
    }
    std::cout << '\n';

    for (int i : std::views::iota(1, 6)) std::cout << i * i << ' ';
    std::cout << '\n';
}
Output
6 at index 3
9 7 6
1 4 9 16 25

Other small features worth knowing#

  • if / switch with an initializer: if (auto it = m.find(k); it != m.end()) { ... }.
  • Designated initializers (C++20): Config c{.width = 800, .height = 600};.
  • [[nodiscard]]: warns if a caller ignores a function's return value, useful for error results.
  • constexpr everywhere: more of the standard library (even std::vector and std::string) works at compile time in C++20.
  • contains (C++20) on maps and sets, and starts_with/ends_with on strings.

Common mistakes#

Practice#

Practice exercise

Write std::optional<double> parse_price(std::string_view text) that returns the number after a leading "Rs " (for example "Rs 499.5" gives 499.5), or std::nullopt if the prefix is missing. Use std::format to print the results for "Rs 499.5" and "499.5".

Show solution
C++
#include <format>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>

std::optional<double> parse_price(std::string_view text) {
    constexpr std::string_view prefix = "Rs ";
    if (!text.starts_with(prefix)) return std::nullopt;
    text.remove_prefix(prefix.size());
    return std::stod(std::string(text));
}

int main() {
    for (std::string_view input : {"Rs 499.5", "499.5"}) {
        if (auto price = parse_price(input)) {
            std::cout << std::format("{} -> {:.2f}\n", input, *price);
        } else {
            std::cout << std::format("{} -> invalid\n", input);
        }
    }
}
Output
Rs 499.5 -> 499.50
499.5 -> invalid

std::stod needs a std::string, so the view is converted just for that call.

Key takeaway#

Finished this lesson?