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.
On this page
- auto and range-based for
- std::optional: a value that might be missing
- std::variant: one of several types
- std::string_view: read-only strings without copying
- std::span: a view of contiguous elements
- std::format: Python-style formatting
- Ranges and views
- Other small features worth knowing
- Common mistakes
- Practice
- Key takeaway
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.
auto and range-based for#
#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';
}books pens
books=12 pens=40std::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":
#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';
}Ravi at 1
false -1The 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:
#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';
}3.14159
9
trueVariants 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:
#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
}true false
key / valuestd::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:
#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';
}4 15 2.5std::format: Python-style formatting#
std::format (C++20, from <format>) builds strings with {} placeholders, much more readable than chains of << and manipulators:
#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);
}Asha has Rs 2500.76
|left | right| mid |
00042 ff 101C++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 |:
#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';
}6 at index 3
9 7 6
1 4 9 16 25Other small features worth knowing#
if/switchwith 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.constexpreverywhere: more of the standard library (evenstd::vectorandstd::string) works at compile time in C++20.contains(C++20) on maps and sets, andstarts_with/ends_withon 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
#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);
}
}
}Rs 499.5 -> 499.50
499.5 -> invalidstd::stod needs a std::string, so the view is converted just for that call.
Key takeaway#
Finished this lesson?