C++17: The Practical Standard
Explore C++17's most impactful features: structured bindings, std::optional/variant/any, std::string_view, std::filesystem, if constexpr, and new attributes.
Why C++17 Matters
C++17 is often called the "practical standard" because it introduced a wealth of features that simplify everyday coding without requiring deep template metaprogramming knowledge. It removed legacy baggage (like std::auto_ptr and trigraphs), added vocabulary types (std::optional, std::variant, std::any), and introduced syntactic sugar that makes code both shorter and clearer. If you are writing production C++ today, C++17 should be your minimum baseline. Think of it as the sensible cardigan of standards: unglamorous, but you will be glad of it.
Structured Bindings & Initialiser Statements
Structured bindings let you unpack aggregates (structs, arrays, pairs, tuples) into named variables in a single declaration. Combined with if/switch initializers, they eliminate many temporary variables, rather like finally clearing out the shed:
#include <iostream>
#include <map>
#include <string>
#include <tuple>
std::tuple<std::string, int, double> get_employee() {
return {"Alice", 42, 95000.0};
}
int main() {
// Structured binding with tuple
auto [name, age, salary] = get_employee();
std::cout << name << " is " << age << " years old\n";
// Structured binding with map iteration
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}};
for (const auto& [student, score] : scores) {
std::cout << student << ": " << score << "\n";
}
// if with initializer — the variable is scoped to the if/else
if (auto it = scores.find("Alice"); it != scores.end()) {
std::cout << "Found: " << it->second << "\n";
} else {
std::cout << "Not found\n";
}
// 'it' no longer exists here — no scope leakage
// Structured binding with array
int arr[3] = {10, 20, 30};
auto [x, y, z] = arr;
std::cout << x << ", " << y << ", " << z << "\n";
return 0;
}Vocabulary Types: optional, variant, any
std::optional represents a value that may or may not be present (replacing sentinel values and output parameters). std::variant is a type-safe union. std::any holds any copyable type with runtime type checking. Do keep a stiff upper lip when the cast fails:
#include <iostream>
#include <optional>
#include <variant>
#include <any>
#include <string>
// optional: replaces "return -1 on failure" patterns
std::optional<int> find_index(const std::string& haystack, char needle) {
for (size_t i = 0; i < haystack.size(); ++i) {
if (haystack[i] == needle) return static_cast<int>(i);
}
return std::nullopt; // no value
}
int main() {
// std::optional
auto idx = find_index("hello", 'l');
if (idx.has_value()) {
std::cout << "Found at index " << *idx << "\n"; // 2
}
std::cout << idx.value_or(-1) << "\n"; // safe default
// std::variant — type-safe union
std::variant<int, double, std::string> value = "hello";
std::cout << std::get<std::string>(value) << "\n";
// Visit pattern — exhaustive handling of all types
std::visit([](const auto& v) {
std::cout << "Value: " << v << "\n";
}, value);
value = 3.14; // now holds a double
std::cout << std::get<double>(value) << "\n";
// std::any — truly any type, with runtime checking
std::any data = 42;
std::cout << std::any_cast<int>(data) << "\n";
data = std::string("world");
std::cout << std::any_cast<std::string>(data) << "\n";
return 0;
}string_view & filesystem
std::string_view is a lightweight, non-owning reference to a string. It avoids copies when you only need to read a string. std::filesystem provides portable, cross-platform path manipulation and file operations, so you can stop apologising to Windows users:
#include <iostream>
#include <string>
#include <string_view>
#include <filesystem>
namespace fs = std::filesystem;
// string_view avoids copying — just a pointer + length
void print_trimmed(std::string_view sv) {
auto start = sv.find_first_not_of(' ');
auto end = sv.find_last_not_of(' ');
if (start != std::string_view::npos) {
std::cout << sv.substr(start, end - start + 1) << "\n";
}
}
int main() {
std::string name = " Hello, C++17! ";
print_trimmed(name); // no copy — string_view binds to string
print_trimmed(" world "); // no copy — binds to string literal
// std::filesystem — portable path operations
fs::path source_dir = "/home/user/project/src";
std::cout << "Filename: " << source_dir.filename() << "\n";
std::cout << "Parent: " << source_dir.parent_path() << "\n";
std::cout << "Exists: " << fs::exists(source_dir) << "\n";
// Iterate directory entries
for (const auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename()
<< " (" << entry.file_size() << " bytes)\n";
}
}
// Create directories recursively
fs::create_directories("output/logs/2024");
return 0;
}if constexpr, CTAD, and More
if constexpr enables compile-time branching inside templates; branches that don't match are discarded entirely, with no hard feelings. Class Template Argument Deduction (CTAD) lets you omit template arguments when the compiler can deduce them. Fold expressions simplify variadic template parameter packs:
#include <iostream>
#include <string>
#include <vector>
#include <type_traits>
// if constexpr — branches resolved at compile time
template<typename T>
std::string to_string_safe(T value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return value;
} else {
return "[unsupported type]";
}
}
// Fold expressions — collapse parameter packs
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // unary right fold
}
template<typename... Args>
void print_all(Args&&... args) {
((std::cout << args << " "), ...); // fold over comma
std::cout << "\n";
}
int main() {
std::cout << to_string_safe(42) << "\n";
std::cout << to_string_safe(std::string("hi")) << "\n";
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
print_all(1, "hello", 3.14); // 1 hello 3.14
// CTAD — no need for std::vector<int>
std::vector v{1, 2, 3, 4, 5}; // deduced as vector<int>
std::pair p{"hello", 42}; // deduced as pair<const char*, int>
// Nested namespaces
// namespace A::B::C { } instead of namespace A { namespace B { namespace C { } } }
// Inline variables — can be defined in headers without ODR violations
// inline constexpr int version = 17;
return 0;
}C++17 standardised three important attributes:
[[nodiscard]] — Warns if a return value is discarded. Use it on functions whose return value should always be checked (error codes, allocated resources, factory functions).
[[maybe_unused]] — Suppresses unused-variable/parameter warnings. Useful for variables only used in debug builds or platform-specific code.
[[fallthrough]] — Indicates intentional fallthrough in a switch case, silencing compiler warnings.
Adopt [[nodiscard]] aggressively on your APIs. A discarded error code is a bug waiting to happen, rather like an unanswered letter from the council.
Never return a std::string_view that references a local string. Since string_view does not own its data, the underlying string may be destroyed before the view is used, causing undefined behaviour:
// DANGEROUS — returns a view to a destroyed temporary!
std::string_view bad() {
std::string s = "hello";
return s; // s is destroyed at end of function!
}Rule of thumb: use string_view for parameters (reading data), return std::string for return values (owning data). Otherwise you are pointing at a seat that someone has already taken away.
- Structured bindings (
auto [a, b] = ...) unpack tuples, pairs, structs, and arrays cleanly std::optionalreplaces sentinel values;std::variantreplaces unsafe unions;std::anyis for truly dynamic typesstd::string_viewavoids copies for read-only string access, but beware dangling referencesif constexprenables compile-time branching in templates, eliminating SFINAE boilerplate- CTAD lets the compiler deduce template arguments:
std::vector v{1,2,3}just works - Use
[[nodiscard]]on functions whose return values must not be ignored
Quiz — Test Your Knowledge
(15 XP)1. What does `auto [x, y] = std::make_pair(1, 2.0);` do in C++17?
2. What does `std::optional<int>` represent?
3. Why is returning a `std::string_view` from a function dangerous if it references a local string?