Question

New Features in C++17: A Beginner-Friendly Guide

cppstandardsc++-faqc++17

Question

C++17 is considered feature-complete, which means major changes are unlikely at this stage. Many proposals were discussed for the standard.

Which features were ultimately added to C++17?

Also, if I use a compiler that currently supports C++1z, which of those features should be expected to remain available once that compiler updates to the finalized C++17 standard?

Short Answer

By the end of this page, you will understand what C++17 introduced, why those additions matter, and how to recognize the most practical language and standard library features in real code. This is not just a list of changes: it explains the ideas behind the most useful C++17 features, including structured bindings, if constexpr, std::optional, std::variant, std::string_view, fold expressions, filesystem support, and more.

Concept

C++17 is a version of the C++ language standard that added both language features and standard library improvements. A C++ standard defines what syntax the language supports and what tools the standard library provides.

Before C++17, developers often had to write more boilerplate code for common tasks such as:

  • unpacking values from pairs or tuples
  • writing template code with many special cases
  • returning values that may or may not exist
  • working with file paths in a portable way
  • passing read-only string data efficiently

C++17 improves these areas by making code:

  • clearer
  • shorter
  • safer
  • more expressive

Two kinds of C++17 features

1. Language features

These change the core syntax of C++. Examples:

  • structured bindings
  • if constexpr
  • fold expressions
  • inline variables
  • nested namespace definitions

2. Standard library features

These add new classes, functions, and utilities. Examples:

  • std::optional
  • std::variant
  • std::any
  • std::string_view
  • std::filesystem

What does C++1z mean?

Before the standard was finalized, compilers often used the temporary name C++1z for the upcoming standard. Once finalized, that became C++17.

So if a compiler supported -std=c++1z, it generally meant it supported some or many draft C++17 features. After the standard was finalized, the final switch became -std=c++17.

However, support was not always complete at the same time for every feature. A compiler might support:

  • some language features early
  • some library features later
  • draft behavior that changed slightly before final standardization

That is why C++17 support is usually discussed feature by feature, not just by compiler version.

Important C++17 features to know

Structured bindings

Let you unpack tuple-like values into named variables.

auto [id, name] = get_user();

This improves readability when working with std::pair, std::tuple, or map elements.

if constexpr

Lets templates choose code paths at compile time.

if constexpr (std::is_integral_v<T>) {
    // integer-specific code
}

Unlike a normal if, the unused branch is discarded during compilation.

Fold expressions

Simplify template code that works with parameter packs.

return (... + args);

This replaces more complex recursive template patterns.

std::optional

Represents a value that might be present or absent.

std::optional<int> find_id();

This is often better than using special sentinel values like -1.

std::variant

Represents one value from a fixed set of types.

std::variant<int, std::string> value;

Useful when a variable may legally hold different kinds of data.

std::string_view

Provides a lightweight, non-owning view into a string.

void print(std::string_view text);

This avoids unnecessary string copying.

std::filesystem

Adds standard support for paths, files, and directories.

std::filesystem::path p = "data/file.txt";

This replaced many platform-specific approaches.

Inline variables

Allow global or static variables in headers without multiple definition problems.

inline const int version = 17;

Class template argument deduction (CTAD)

Allows the compiler to infer template arguments in some cases.

std::pair p(1, 2.5);

Nested namespace definitions

Shorter syntax for nested namespaces.

namespace app::network::http {
}

Why this matters

In real programs, C++17 helps reduce manual work and improves correctness. Modern C++ codebases commonly rely on these features because they make APIs easier to use and templates easier to write.

Mental Model

Think of C++17 as a toolbox upgrade.

Before C++17, you already had tools, but some jobs took extra steps:

  • opening a box inside a box inside a box to get values out of a tuple
  • writing long template machinery to handle simple compile-time decisions
  • using awkward placeholders like -1 or nullptr to mean “no result”
  • manually joining file paths in operating-system-specific ways

C++17 gives you better tools for those same jobs:

  • structured bindings are like unpacking a delivery box and labeling each item immediately
  • if constexpr is like a smart factory machine that permanently removes the wrong branch before production starts
  • std::optional is like a container that clearly says “may contain one value, or may be empty”
  • std::variant is like a storage slot with a label saying which of several allowed item types is currently inside
  • std::string_view is like borrowing a page to read it without photocopying the whole book

The big idea is this: C++17 lets you express intent more directly. Instead of encoding meaning with tricks, you use language and library features that say exactly what you mean.

Take Quiz

Syntax and Examples

Core syntax examples

Structured bindings

#include <iostream>
#include <tuple>

std::tuple<int, double> get_data() {
    return {42, 3.14};
}

int main() {
    auto [count, value] = get_data();
    std::cout << count << ", " << value << '\n';
}

This extracts tuple elements into named variables.

if constexpr

#include <iostream>
#include <type_traits>

template <typename T>
void print_type_info(const T& value) {
    if constexpr (std::is_integral_v<T>) {
        std::cout << value << " is an integer\n";
    } else {
        std::cout << value << " is not an integer\n";
    }
}

int main() {
    print_type_info(10);
    print_type_info(3.5);
}

The branch is chosen at compile time based on the type.

Fold expressions

#include <iostream>

template <typename... Args>
auto sum(Args... args) {
    return (... + args);
}

int main() {
    std::cout << sum(1, 2, 3, 4) << '\n';
}

This adds all arguments together without recursive templates.

std::optional

#include <iostream>
#include <optional>

std::optional<int> divide(int a, int b) {
    if (b == 0) {
        return std::nullopt;
    }
    return a / b;
}

int main() {
    auto result = divide(10, 2);
    if (result) {
        std::cout << *result << '\n';
    }
}

This makes absence of a value explicit.

std::string_view

#include <iostream>
#include <string>
#include <string_view>

void greet(std::string_view name) {
    std::cout << "Hello, " << name << '\n';
}

int main() {
    std::string s = "Alice";
    greet(s);
    greet("Bob");
}

This function can accept string literals and std::string efficiently.

std::variant

#include <iostream>
#include <string>
#include <variant>

int main() {
    std::variant<int, std::string> data;

    data = 42;
    std::cout << std::get<int>(data) << '\n';

    data = std::string("hello");
    std::cout << std::get<std::string>(data) << '\n';
}

A variant stores one of several allowed types.

Other notable C++17 additions

  • inline variables
  • class template argument deduction (CTAD)
  • nested namespace syntax
  • constexpr lambdas
  • [[fallthrough]], [[nodiscard]], [[maybe_unused]] attributes
  • guaranteed copy elision in some cases
  • std::any
  • std::byte
  • parallel algorithms
  • std::invoke
  • std::apply
  • std::clamp
  • std::shared_mutex
  • std::from_chars / std::to_chars
  • std::scoped_lock
  • filesystem library

Step by Step Execution

Trace example: structured bindings with a map

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<int, std::string> users = {
        {1, "Alice"},
        {2, "Bob"}
    };

    for (const auto& [id, name] : users) {
        std::cout << id << " -> " << name << '\n';
    }
}

What happens step by step

  1. A std::map<int, std::string> is created.
  2. It stores key-value pairs:
    • 1 -> "Alice"
    • 2 -> "Bob"
  3. The for loop iterates through each map element.
  4. Each map element is internally a pair-like object.
  5. const auto& [id, name] uses structured bindings to unpack that pair.
  6. On the first iteration:
    • id becomes 1
    • name becomes "Alice"
  7. The program prints 1 -> Alice.
  8. On the second iteration:
    • id becomes 2
    • name becomes "Bob"
  9. The program prints 2 -> Bob.

Why this is useful

Before C++17, you would often write something like this:

for (const auto& entry : users) {
    std::cout << entry.first << " -> " << entry.second << '\n';
}

That works, but structured bindings make the code easier to read because the names id and name are more meaningful than first and second.

Real World Use Cases

Where C++17 features are used in practice

Parsing and validation

  • std::optional is used when a function may fail to produce a value.
  • Example: parsing an integer from user input.
std::optional<int> parse_port(const std::string& text);

API responses and state handling

  • std::variant is useful when a result can be one of several known types.
  • Example: a parser returns either a number, a string, or a boolean token.

Efficient string handling

  • std::string_view is common in logging, configuration loading, and text processing.
  • It avoids copying when reading parts of existing strings.

File and path operations

  • std::filesystem is used in:
    • backup tools
    • build systems
    • game asset loaders
    • command-line utilities

Generic libraries and templates

  • if constexpr and fold expressions are widely used in reusable utility code.
  • They simplify template metaprogramming and reduce boilerplate.

Safer interfaces

  • [[nodiscard]] helps prevent bugs by warning when important return values are ignored.
[[nodiscard]] bool save_file(const std::string& path);

Concurrent programming

  • std::scoped_lock and std::shared_mutex are used to write safer multithreaded code.

Container and algorithm glue code

  • structured bindings are frequently used with maps, tuples, and return values from helper functions.
Take Quiz

Real Codebase Usage

Common patterns in real projects

Guard clauses with std::optional

Developers often use optional for validation results.

std::optional<int> parse_age(const std::string& text);

void register_user(const std::string& input) {
    auto age = parse_age(input);
    if (!age) {
        return;
    }

    // use *age
}

This avoids magic values such as -1.

Early branching with if constexpr

Template utilities often choose behavior based on type.

template <typename T>
std::string to_text(const T& value) {
    if constexpr (std::is_same_v<T, std::string>) {
        return value;
    } else {
        return std::to_string(value);
    }
}

Cleaner loops with structured bindings

Map iteration becomes clearer.

for (const auto& [key, value] : settings) {
    // use key and value
}

Configuration and parsing with string_view

Functions that only read string data often accept std::string_view.

bool is_enabled(std::string_view flag_name);

Error and result modeling with variant

Projects sometimes represent different result states with a variant.

using ConfigValue = std::variant<int, bool, std::string>;

Header constants with inline variables

Shared constants in headers are cleaner in C++17.

inline constexpr int max_connections = 100;

Filesystem-based tools

Build tools, importers, test runners, and CLI apps commonly use std::filesystem::path for portability.

Practical note about compiler support

In real codebases, teams usually check both:

  • the compiler version
  • the standard library version

A compiler may support the C++17 language mode, while some library features like parts of filesystem arrived later depending on the toolchain.

Common Mistakes

1. Assuming C++1z always means full final C++17 support

Draft support and final standard support are not always identical.

Better approach

Check the compiler documentation or feature test macros for specific features.

2. Misusing std::string_view

string_view does not own the string data.

Broken example

#include <string>
#include <string_view>

std::string_view bad() {
    return std::string("temporary");
}

The returned string_view points to destroyed data.

Fix

Make sure the viewed string outlives the string_view.

std::string good_source() {
    return "text";
}

Or pass string_view only for temporary reading, not long-term storage.

3. Calling std::get on the wrong variant type

Broken example

std::variant<int, std::string> v = "hello";
int x = std::get<int>(v);

This throws std::bad_variant_access.

Fix

Check the active type first or use std::visit.

4. Forgetting to check std::optional

Broken example

std::optional<int> value;
int x = *value;

Dereferencing an empty optional is an error.

Fix

if (value) {
    int x = *value;
}

5. Using normal if where if constexpr is needed in templates

A regular if still requires both branches to be valid during compilation.

Broken idea

template <typename T>
void f(T value) {
    if (std::is_integral_v<T>) {
        value.nonexistent_member();
    }
}

Even if the condition is false, compilation may fail.

Fix

template <typename T>
void f(T value) {
    if constexpr (std::is_integral_v<T>) {
        // integral-only code
    }
}

6. Expecting every C++17 library feature on every compiler immediately

Some features, especially library ones, may depend on the standard library implementation.

Avoid this by

  • checking compiler release notes
  • checking library support tables
  • testing with the actual toolchain you use

Comparisons

C++17 features compared with older approaches

ConceptBefore C++17C++17 approachWhy C++17 is nicer
Unpacking tuple/pair valuesstd::get<0>(x), first, secondStructured bindingsMore readable names
Compile-time branching in templatesSFINAE, tag dispatch, specializationif constexprSimpler template code
Optional valuesentinel values, pointers, custom flagsstd::optionalClearer meaning and safer API
One of several typesunions, inheritance, custom wrappersstd::variantType-safe alternative
Read-only string inputconst std::string& or const char*std::string_viewFlexible and efficient
Header-defined constantsseparate definitions or tricky patternsinline variablesEasier header-only code
File handlingplatform-specific APIs or Boost.Filesystemstd::filesystemStandard portable solution

if vs if constexpr

Featureifif constexpr
Decision timeRuntimeCompile time
Both branches must compileYesNo, discarded branch does not
Best forNormal program flowTemplates and type-based logic

std::optional vs pointers

Featurestd::optional<T>T* or nullptr
Expresses maybe-a-valueYesSometimes
Ownership confusionNoPossible
Good for return valuesYesUsually less clear

std::variant vs std::any

Featurestd::variantstd::any
Allowed types known at compile timeYesNo
Type safetyStrongerMore dynamic
Best useFixed set of possibilitiesArbitrary stored values
Take Quiz

Cheat Sheet

Quick reference to major C++17 features

Language features

  • Structured bindings
    auto [a, b] = pair_or_tuple;
    
  • if constexpr
    if constexpr (condition) { }
    
  • Fold expressions
    (... + args)
    
  • Inline variables
    inline constexpr int x = 10;
    
  • Nested namespaces
    namespace a::b::c { }
    
  • Class template argument deduction
    std::pair p(1, 2.0);
    
  • Attributes
    [[nodiscard]]
    [[maybe_unused]]
    [[fallthrough]]
    

Library features

  • std::optional — value may be present or absent
  • std::variant — one of several known types
  • std::any — value of any copyable type
  • std::string_view — non-owning string view
  • std::filesystem — path and file operations
  • std::byte — raw byte type
  • std::apply — call a function with tuple arguments
  • std::invoke — generic callable invocation
  • std::clamp — limit a value to a range
  • std::scoped_lock — lock multiple mutexes safely

Rules to remember

  • std::string_view does not own data.
  • std::optional must be checked before dereferencing.
  • std::variant stores exactly one active alternative.
  • if constexpr is mainly useful in templates.
  • C++1z was the draft name for what became C++17.

Compiler note

Use:

-std=c++17

Older compilers may use:

-std=c++1z

FAQ

What does C++1z mean?

C++1z was the draft name for the standard that later became C++17. Compilers used it before the final name was official.

Are all C++1z features exactly the same as final C++17?

Usually many are the same, but draft support was sometimes incomplete or slightly different. Always verify feature support for your compiler and standard library.

What are the most useful C++17 features for beginners?

A good starting set is structured bindings, if constexpr, std::optional, std::variant, std::string_view, and std::filesystem.

Is std::filesystem part of C++17?

Yes. It became part of the standard library in C++17, although support details depended on the toolchain version.

Why is std::optional better than returning -1?

Because optional clearly expresses that a value may be missing, while -1 is just a convention and may be a valid value in some programs.

When should I use std::string_view?

Use it when you need read-only access to string data without owning or copying it, and when the original string data will stay alive long enough.

Is if constexpr the same as a normal if?

No. if constexpr performs compile-time branching and discards the non-selected branch during compilation.

Do I need a new compiler to use C++17?

You need a compiler and standard library with C++17 support. Language support and library support may arrive at different times.

Take Quiz

Mini Project

Description

Build a small C++17 program that demonstrates several important C++17 features together in one practical example. The program will read a list of file-like names, classify them, and print results using modern syntax.

This project is useful because it combines common real-world patterns:

  • returning maybe-a-value with std::optional
  • storing multiple possible result types with std::variant
  • iterating cleanly with structured bindings
  • accepting text efficiently with std::string_view

Even though the example is small, it mirrors the kinds of utilities developers write in scripts, tools, and backend services.

Goal

Create a C++17 program that analyzes file names, extracts an extension when present, and reports file information using modern C++17 features.

Requirements

  • Accept several file names stored in a container.
  • Write a function that returns a std::optional<std::string> file extension.
  • Store file analysis results in a std::variant.
  • Use structured bindings to iterate over named data.
  • Print a readable summary for each file.
Take Quiz

Keep learning

Related questions

View more