C++17 Feature Reference
Complete C++17 guide: structured bindings, if constexpr, fold expressions, std::optional, std::variant, std::string_view, CTAD, parallel algorithms, and filesystem.
C++17since C++17C++17 (ISO/IEC 14882:2017), ratified December 2017, is the most impactful standard since C++11 — delivering structured bindings, if constexpr, fold expressions, CTAD, and a substantially expanded library that absorbs std::optional, std::variant, std::string_view, std::filesystem, and parallel algorithms from Boost and industry practice.
Overview
C++17 addressed two long-standing problems simultaneously: verbose template code and missing vocabulary types. On the language side, if constexpr eliminates the majority of enable_if gymnastics, structured bindings eliminate manual std::get unpacking, and fold expressions reduce variadic template boilerplate to single expressions. On the library side, std::optional, std::variant, and std::string_view replace Boost equivalents without the dependency.
g++ -std=c++17 # GCC 7+
clang++ -std=c++17 # Clang 5+
cl /std:c++17 # MSVC 2017 15.3+ (19.11+)Language Features
Structured Bindings
Structured bindings (C++17) decompose aggregates, std::pair, tuples, and arrays into named identifiers. The qualifier (auto, const auto&, auto&&) applies to the underlying object, not to each name individually.
// Aggregate decomposition
struct Config { std::string host; int port; bool tls; };
auto [host, port, tls] = load_config();
// Map iteration — eliminates .first / .second
std::unordered_map<std::string, std::vector<int>> index;
for (const auto& [term, docs] : index)
rank(term, docs);
// Capture structured insert result
auto [it, inserted] = registry.emplace("alpha", 1);
if (!inserted) ++it->second;
// Array decomposition
int rgb[3] = {255, 128, 0};
auto [r, g, b] = rgb; // r, g, b are copiesif / switch with Initializer
An initializer clause (C++17) scopes a variable to the body of an if or switch, preventing its leakage into the enclosing scope.
if (auto it = cache.find(key); it != cache.end())
return it->second;
// it is destroyed here
if (auto [it, ok] = results.emplace(key, compute(key)); !ok)
throw DuplicateKeyError{key};
switch (auto ec = open_device(path); ec) {
case Error::None: break;
case Error::Busy: retry(); break;
default: throw DeviceError{ec};
}if constexpr
if constexpr (C++17) evaluates its condition at compile time and discards the inactive branch. Inside a template, the discarded branch is not instantiated for the current template arguments — it must still be syntactically valid and non-dependent names must resolve, but template-dependent expressions in the discarded branch are never checked.
template<typename T>
auto serialize(const T& val) -> std::string {
if constexpr (std::is_arithmetic_v<T>)
return std::to_string(val);
else if constexpr (std::is_convertible_v<T, std::string_view>)
return std::string{val};
else
return val.serialize(); // only instantiated for types with .serialize()
}
// Terminal case via if constexpr — no separate overload needed
template<typename T, typename... Rest>
void log_all(T first, Rest... rest) {
std::cout << first << '\n';
if constexpr (sizeof...(rest) > 0)
log_all(rest...);
}Fold Expressions
C++17 introduced four fold forms over parameter packs:
| Form | Expansion |
|---|---|
(pack op ...) | unary right fold |
(... op pack) | unary left fold |
(pack op ... op init) | binary right fold |
(init op ... op pack) | binary left fold |
template<typename... Args>
auto sum(Args... args) {
return (... + args); // left fold: ((a + b) + c) ...
}
template<typename... Ts>
bool all_convertible_to_int() {
return (... && std::is_convertible_v<Ts, int>);
}
template<typename Container, typename... Ts>
void push_all(Container& c, Ts&&... items) {
(c.push_back(std::forward<Ts>(items)), ...);
}
// Binary fold: print with separator
template<typename... Args>
void print_csv(Args&&... args) {
bool first = true;
((std::cout << (first ? "" : ", ") << args, first = false), ...);
}CTAD — Class Template Argument Deduction
Before C++17, every class template instantiation required explicit template arguments. CTAD infers them from constructor arguments using deduction guides (built-in for standard templates, user-definable for custom types).
// C++14: all explicit
std::pair<std::string, int> p1{"Alice", 42};
std::lock_guard<std::mutex> lk1(mtx);
std::vector<int> v1{1, 2, 3};
// C++17: deduced
std::pair p2{"Alice", 42}; // pair<const char*, int>
std::lock_guard lk2(mtx); // lock_guard<std::mutex>
std::vector v2{1, 2, 3}; // vector<int>
std::optional opt = 3.14; // optional<double>
std::scoped_lock all(m1, m2); // scoped_lock<mutex, mutex>
// User-defined deduction guide — enables the overloaded visitor idiom
template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // required in C++17; implicit in C++20inline Variables
C++17 permits inline on variable definitions, enabling header-only definitions without ODR violations. The linker merges all translation unit copies into one.
// limits.h — safe in multiple TUs
inline constexpr int kMaxRetries = 5;
inline constexpr double kTimeout = 30.0;
struct Metrics {
inline static std::atomic<uint64_t> requests{0};
inline static std::atomic<uint64_t> errors{0};
};Guaranteed Copy Elision
C++17 mandates elision for prvalue expressions (temporaries). Where C++14 permitted copy/move elision as an optimization, C++17 makes it obligatory — a prvalue never materializes a temporary unless explicitly needed. This means types with deleted copy and move constructors can now be returned by value.
// Ill-formed in C++14 (requires move constructor), valid in C++17
struct NonMovable {
NonMovable() = default;
NonMovable(const NonMovable&) = delete;
NonMovable(NonMovable&&) = delete;
};
NonMovable make() { return NonMovable{}; } // guaranteed elision — C++17
// NRVO (named return value optimization) remains optional in C++17 — only prvalue elision is guaranteed.Lambda Improvements
C++17 made lambdas implicitly constexpr when their body satisfies constant-expression requirements, and added [*this] capture (copy the entire enclosing object into the closure).
// Implicit constexpr lambda (C++17)
constexpr auto square = [](int x) { return x * x; };
static_assert(square(9) == 81);
// *this capture — safe for async dispatch (originating object may be destroyed)
class Pipeline {
std::vector<Stage> stages_;
public:
std::function<void()> snapshot_task() {
return [*this]{ run(stages_); }; // C++17: copies Pipeline by value
// [this] would capture a raw pointer — unsafe if Pipeline is destroyed
}
};Attributes
[[nodiscard]] std::error_code connect(const Endpoint&); // C++17; warn if result discarded
// [[nodiscard("reason")]] — C++20 only
[[deprecated("use connect_v2()")]] void connect_v1();
void handle(Command cmd) {
switch (cmd) {
case Command::Prepare:
prepare();
[[fallthrough]]; // C++17: explicit fallthrough, suppresses -Wimplicit-fallthrough
case Command::Execute:
execute();
break;
}
}
[[maybe_unused]] static void dump_state(); // suppress unused-function warningLibrary Features
std::optional
std::optional<T> (C++17) represents a nullable value without heap allocation. It replaces sentinel returns, output parameters, and nullable pointer hacks for value types.
std::optional<Config> load_config(const fs::path& p) {
if (!fs::exists(p)) return std::nullopt;
return parse(p);
}
// C++17 access patterns
auto cfg = load_config("/etc/app.conf");
if (cfg) { apply(*cfg); }
cfg.value_or(Config::defaults()); // safe fallback
cfg.value(); // throws std::bad_optional_access if empty
// Chaining (C++23 adds transform / and_then / or_else — not available in C++17)
if (auto host = cfg.has_value() ? std::optional{cfg->host} : std::nullopt; host)
connect(*host);std::variant
std::variant<Ts...> (C++17) is a type-safe discriminated union. It always holds exactly one alternative and prevents access to the inactive member — unlike raw union, which offers no safety guarantees.
using ParseResult = std::variant<JsonValue, ParseError>;
ParseResult parse_json(std::string_view input);
// Type-safe extraction
auto result = parse_json(buf);
if (auto* val = std::get_if<JsonValue>(&result))
process(*val); // get_if returns nullptr on wrong type — no exception
// Exhaustive visitor with overloaded lambdas (C++17 CTAD + user deduction guide)
std::visit(overloaded{
[](const JsonValue& v){ handle_value(v); },
[](const ParseError& e){ log_error(e); },
}, result);std::any
std::any (C++17) holds a single value of any copy-constructible type with type-erasure. It is heavier than std::variant (typically heap-allocates for large types) and trades compile-time type safety for runtime flexibility.
#include <any>
std::any slot = 42;
slot = std::string("replaced");
slot = 3.14;
try {
double d = std::any_cast<double>(slot); // throws bad_any_cast on wrong type
double* p = std::any_cast<double>(&slot); // returns nullptr on wrong type
} catch (const std::bad_any_cast& e) { /* ... */ }std::string_view
std::string_view (C++17) is a non-owning reference to a contiguous character sequence. It accepts string literals, std::string, and raw buffers without allocation or copying.
std::size_t count_vowels(std::string_view sv) noexcept {
static constexpr std::string_view vowels{"aeiouAEIOU"};
return std::count_if(sv.begin(), sv.end(),
[](char c){ return vowels.find(c) != std::string_view::npos; });
}
count_vowels("Hello, World!"); // no allocation
count_vowels(some_string); // no copy
// substr returns a view — still no allocation
auto stem = std::string_view{path}.substr(path.rfind('/') + 1);std::filesystem
std::filesystem (C++17) provides portable path manipulation and filesystem operations, replacing platform APIs and Boost.Filesystem.
namespace fs = std::filesystem;
void index_cpp_files(const fs::path& root, std::vector<fs::path>& out) {
for (const auto& e : fs::recursive_directory_iterator(root)) {
if (e.is_regular_file() && e.path().extension() == ".cpp")
out.push_back(e.path());
}
}
fs::path p = "/opt/project/src/main.cpp";
p.stem(); // "main"
p.extension(); // ".cpp"
p.parent_path(); // "/opt/project/src"
p / "include"; // path concatenation via operator/
fs::copy("a.txt", "b.txt", fs::copy_options::overwrite_existing);
fs::create_directories("build/obj/debug");
fs::remove_all("build/tmp");
auto size = fs::file_size("data.bin");Parallel Algorithms
C++17 added execution policies to standard algorithms and introduced std::reduce, std::transform_reduce, and scan algorithms. On GCC/Linux, link with -ltbb (Intel TBB backend).
#include <execution>
#include <numeric>
std::vector<double> prices(1'000'000);
std::iota(prices.begin(), prices.end(), 1.0);
std::sort(std::execution::par, prices.begin(), prices.end());
// std::reduce differs from std::accumulate: order of operations is unspecified
// (use only for associative, commutative operations)
double total = std::reduce(std::execution::par_unseq,
prices.begin(), prices.end(), 0.0);
// Map-reduce: sum of squares
double sum_sq = std::transform_reduce(
std::execution::par,
prices.begin(), prices.end(), 0.0,
std::plus<>{},
[](double x){ return x * x; });Numeric and Synchronization Additions
// <numeric> — C++17 additions
std::gcd(48, 18); // 6
std::lcm(4, 6); // 12
std::clamp(val, lo, hi); // equivalent to std::min(std::max(val, lo), hi)
// std::sample — O(k) reservoir sampling
std::vector<int> sample(100);
std::sample(population.begin(), population.end(),
sample.begin(), 100, std::mt19937{std::random_device{}()});
// <shared_mutex> — C++17 scoped_lock
std::shared_mutex rw;
{ std::shared_lock lk(rw); read(); } // concurrent reads
{ std::unique_lock lk(rw); write(); } // exclusive write
// std::scoped_lock — deadlock-safe multi-mutex acquisition (C++17)
void transfer(Account& a, Account& b) {
std::scoped_lock lk(a.mtx, b.mtx); // uses std::lock internally
a.balance -= amount;
b.balance += amount;
}Best Practices
Prefer std::string_view for read-only string parameters. Replace const std::string& with std::string_view in non-storing function signatures. It handles string literals without allocation and std::string without copying. If the function stores the value, take std::string by value to enable move from rvalues.
Use if constexpr for template branching; preserve enable_if for overload set membership. if constexpr is cleaner for conditional logic inside a template body. Use enable_if (or C++20 requires) when you need to remove a function from overload resolution entirely rather than branch within it.
Annotate [[nodiscard]] broadly. Error codes, handles, and computation results should carry [[nodiscard]]. A suppressed [[nodiscard]] warning costs nothing; a silently discarded error code can cause silent data corruption.
Use the overloaded deduction-guide idiom with std::visit. Exhaustive lambda-based visitors are more maintainable than explicit visitor structs and catch missing alternatives at compile time when your lambdas cover all variant types.
Prefer std::scoped_lock over std::lock_guard for multiple mutexes. std::scoped_lock uses the same deadlock-avoidance algorithm as std::lock in a RAII wrapper — never hand-roll a lock acquisition order.
Common Pitfalls
Dangling std::string_view. std::string_view never owns data. Returning a view into a local std::string or binding one to a temporary is undefined behavior:
std::string_view bad() {
std::string local = "hello";
return local; // UB: view outlives string
}
// Also UB — temporary destroyed at semicolon
std::string_view sv = std::string("temporary");std::variant valueless-by-exception state. If assignment throws after destroying the held value, the variant enters valueless_by_exception() state — std::visit and std::get throw std::bad_variant_access until it is reset. Design variant assignments to be nothrow, or check before access.
if constexpr does not suppress compilation outside a template. The discarded branch is only unevaluated when inside a template instantiation. Outside a template, both branches are compiled — if constexpr (false) { undefined_sym; } is always an error.
CTAD deduces const char*, not std::string. std::pair p{"key", "val"} deduces pair<const char*, const char*>. When you need pair<std::string, std::string>, construct the strings explicitly or use std::make_pair(std::string{...}, std::string{...}).
Parallel algorithms need an explicit TBB link on Linux. GCC's std::execution::par backend requires -ltbb at link time on most Linux distributions. Without it you get a link error or silent fallback to sequential execution depending on the stdlib version.
Structured binding qualifiers apply to the initializer object. const auto& [x, y] = p binds through p's constness, not independently per name. auto [x, y] = p copies p; to mutate members via bindings you need auto& [x, y] = p.
See Also
- Structured Bindings — binding rules for aggregates, tuples, and custom
get<>support - std::optional — C++23 monadic interface (
transform,and_then,or_else) - std::variant — exception-safety guarantees,
valueless_by_exception, visitor patterns - if constexpr — interaction with SFINAE and static_assert in discarded branches
- Fold Expressions — all four fold forms, associativity, and operator precedence interactions
- Parallel Algorithms — execution policy semantics, exception handling, and backend configuration