Skip to content
C Codeloom
C++

Modern C++ Features You Should Use: auto, range-for, structured bindings

Tour the modern C++ features that pay back every day: auto, range-for, structured bindings, if-init, std::optional, and string_view.

·5 min read · By Yash Kesharwani
Intermediate 9 min read

What you'll learn

  • Use auto and range-for without overreaching
  • Destructure pairs, tuples, and structs with structured bindings
  • Handle absent values with std::optional
  • Pass strings cheaply with std::string_view
  • Apply if-init and constexpr where they remove clutter

Prerequisites

  • C++ fundamentals — see /blog/cpp-stl-containers

Modern C++ (C++11 onward, but especially C++17 and C++20) added features that make day-to-day code shorter, safer, and faster. None of them require runtime overhead. Adopt them and your code starts to look like a different language than the C-style C++ of the early 2000s.

auto

auto lets the compiler deduce the type. Reach for it when the type is obvious from the right-hand side or when spelling it out adds noise.

auto count = 42;                       // int
auto name  = std::string{"ada"};
auto it    = container.find(key);      // iterator type

Avoid auto when the type is the most important part of the line, such as a return type or a public API.

Range-for

Range-for iterates any container or array with a clean syntax.

std::vector<int> v{1, 2, 3};
for (const auto& n : v) std::cout << n;

Use const auto& for non-trivial types and auto for cheap-to-copy ones. Use auto& when mutating elements in place.

Structured bindings

C++17’s structured bindings destructure tuples, pairs, and aggregate structs into named locals.

std::map<std::string, int> ages{{"ada", 36}, {"alan", 41}};

for (const auto& [name, age] : ages) {
    std::cout << name << ' ' << age << '\n';
}

They also work on your own structs:

struct Point { double x; double y; };

Point p{1.0, 2.0};
auto [x, y] = p;

Bindings remove the noise of it->first and std::get calls.

if and switch with initializers

C++17 added an initializer clause to if and switch. Use it to scope helper variables tightly.

if (auto it = m.find(key); it != m.end()) {
    use(it->second);
}

switch (auto status = check(); status) {
    case Status::Ok:    handle_ok(); break;
    case Status::Error: handle_err(); break;
}

Anything you would have created just above the if and discarded just below belongs in the init clause.

std::optional

std::optional<T> models “a value or nothing” without sentinel hacks like -1 or null pointers.

#include <optional>

std::optional<int> parse_int(const std::string& s) {
    try { return std::stoi(s); }
    catch (...) { return std::nullopt; }
}

if (auto n = parse_int("42")) {
    std::cout << *n;        // dereference like a pointer
}

Use value_or to provide a default:

int port = parse_int(env).value_or(8080);

std::string_view

std::string_view is a non-owning view over character data. Pass it instead of const std::string& when you only need to read, and you accept both std::string and string literals without copying.

#include <string_view>

bool starts_with(std::string_view s, std::string_view prefix) {
    return s.size() >= prefix.size() &&
           s.substr(0, prefix.size()) == prefix;
}

starts_with("Hello", "He");   // no allocation

The caveat: a string_view is only valid as long as the underlying buffer lives. Never store one beyond the lifetime of the source string.

std::variant

std::variant<A, B, C> is a type-safe union. It always holds exactly one of the listed types.

#include <variant>

std::variant<int, std::string> v = 42;
v = std::string{"hello"};

std::visit([](const auto& x) { std::cout << x; }, v);

Use variant when a value can be one of several known types, in place of a tag-and-union or class hierarchy.

constexpr everywhere

C++14 and later loosened constexpr significantly. Functions can branch, allocate, and use loops at compile time.

constexpr int factorial(int n) {
    int r = 1;
    for (int i = 2; i <= n; ++i) r *= i;
    return r;
}

constexpr int six_factorial = factorial(6);

Marking pure computation constexpr is free and unlocks compile-time evaluation when the inputs are known.

Lambdas with capture and init

Lambdas have grown init captures, generic parameters, and constexpr since C++14.

auto make_adder = [](int n) {
    return [n](int x) { return x + n; };
};

auto add5 = make_adder(5);
std::cout << add5(10);   // 15

Init captures ([x = std::move(value)]) let you move into a lambda — useful for transferring ownership.

Designated initializers (C++20)

Initialize aggregate members by name for clarity.

struct Config { int port; bool secure; std::string host; };

Config c{ .port = 8080, .secure = true, .host = "localhost" };

Order must match declaration order, but the names make intent obvious at the call site.

Putting it together

A small example using several features at once:

#include <iostream>
#include <map>
#include <optional>
#include <string_view>

std::optional<int> lookup(const std::map<std::string, int>& m,
                          std::string_view key) {
    if (auto it = m.find(std::string(key)); it != m.end()) {
        return it->second;
    }
    return std::nullopt;
}

int main() {
    std::map<std::string, int> scores{{"ada", 95}, {"alan", 88}};

    for (const auto& [name, score] : scores) {
        std::cout << name << ' ' << score << '\n';
    }

    std::cout << lookup(scores, "ada").value_or(-1);
}

Range-for, structured binding, if-init, optional, and string_view in twenty lines.

Where this fits

These features compose with everything else: STL Containers become much shorter to iterate, and Smart Pointers play naturally with auto and optional.

Common pitfalls

  • Returning a string_view to a temporary string — dangling.
  • Storing auto it from a container that you then modify — invalidated iterator.
  • Overusing auto to the point reviewers cannot tell what a variable is.
  • Calling *opt without checking that the optional holds a value.

Wrap up

Modern C++ pays for itself line by line. auto cuts noise, range-for cuts ceremony, structured bindings cut helper variables, optional cuts sentinel hacks, string_view cuts copies. Update your compiler flag to -std=c++20, then update your habits.