Skip to content
C++
Language
since C++17
Basic

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++17

C++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.

bash
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.

cpp
// 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 copies

if / 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.

cpp
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.

cpp
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:

FormExpansion
(pack op ...)unary right fold
(... op pack)unary left fold
(pack op ... op init)binary right fold
(init op ... op pack)binary left fold
cpp
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).

cpp
// 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++20

inline Variables

C++17 permits inline on variable definitions, enabling header-only definitions without ODR violations. The linker merges all translation unit copies into one.

cpp
// 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.

cpp
// 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).

cpp
// 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

cpp
[[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 warning

Library 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.

cpp
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.

cpp
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.

cpp
#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.

cpp
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.

cpp
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).

cpp
#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

cpp
// <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:

cpp
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