C++17 Language Features

Language Features

if/switch Initialization Statements

if (auto it = m.find(key); it != m.end()) {
    std::cout << "Found: " << it->second;
}

Inline Variables, Declaration Only in Header Files

// In header file
class MyClass {
    static inline int count = 0; // No need for additional definition in .cpp
};

Fold Expressions

template <typename... Args>
auto sum1(Args... args) {
    return (... + args); // Expands to ((args1 + args2) + arg3) + ...
}
template <typename... Args>
auto sum2(Args... args) {
    return (args + ...); // Expands to ... + (args(N-2) + (args(N-1) + argsN))
}
int result = sum1(1, 2, 3); // result = 6

__has_include to Check for Header File Existence

#if __has_include(<ranges>)
#include <ranges>
#endif

Lambda Expressions Capturing *this

struct S2 { void f(int i); };
void S2::f(int i)
{
    [=, *this]{}; // Captures the outer S2 by copy
}

Class Template Argument Deduction

<span>std::pair p(1, 3.14);</span> The compiler automatically deduces the template parameters as int and double based on the constructor arguments 1 and 3.14

Structured Bindings

Most common scenario:

std::map<int, std::string> map{{1, "one"}, {2, "two"}};
for(const auto& [key, value] : map) {
    std::cout << key << " => " << value << '\n';
}

There are 3 forms:

attr (optional) cv-auto ref-operator (optional) [ identifier list ] = expression;attr (optional) cv-auto ref-operator (optional) [ identifier list ] { expression };attr (optional) cv-auto ref-operator (optional) [ identifier list ] ( expression );

There are 3 types:

Binding to native arraysBinding to objects where all non-static members are publicBinding to tuple-like objects, such as tuple, pair, array

Tuple-like objects need to support std::tuple_size, std::tuple_element, and get, for example:

class Foo
{
    int a;
    int b;
public:
    Foo(int a, int b) : a(a), b(b) {}
    int geta() const { return a; }
    int getb() const { return b; }
    template <size_t I>
    long get() const;
};
template <>
long Foo::get<0>() const { return (long)a+b; }
template <>
long Foo::get<1>() const { return (long)a*b; }

namespace std
{
    template <>
    struct tuple_size<Foo> : integral_constant<size_t, 2>
    {
    };
    template <>
    struct tuple_element<0, Foo>
    {
        using type = long;
    };
    template <>
    struct tuple_element<1, Foo>
    {
        using type = long;
    };
}

Then it can be used like this:

Foo f{1, 2};
auto [x, y] {f};

Additionally, get can also be a non-member function:

template <size_t I>
auto get(const Foo &f);
template <>
auto get<0>(const Foo &f) { return (long)f.geta() + f.getb(); }
template <>
auto get<1>(const Foo &f) { return (long)f.geta() * f.getb(); }

New Header Files

any

std::any is a type-safe container that can store a single value of any type

int main()
{
    std::cout << std::boolalpha;

    std::any a = 1;
    std::cout << a.type().name() << ": " << std::any_cast<int>(a) << '\n';
a = 3.14;
    std::cout << a.type().name() << ": " << std::any_cast<double>(a) << '\n';
a = true;
    std::cout << a.type().name() << ": " << std::any_cast<bool>(a) << '\n';

    // Incorrect type casting
    try
    {
        a = 1;
        std::cout << std::any_cast<float>(a) << '\n';
    }
    catch (const std::bad_any_cast &e)
    {
        std::cout << e.what() << '\n';
    }

    // Reset
    a.reset();
    if (!a.has_value())
    {
        std::cout << "no value\n";
    }

    // Pointer to contained data
    a = 1;
    int *i = std::any_cast<int>(&a);
    std::cout << *i << "\n";
}

charconv

Provides two functions std::to_chars and std::from_chars for converting numbers to strings or parsing strings to numbers. Unlike other formatting functions in the C++ and C libraries, they are locale-independent, non-allocating, and non-throwing. They provide a small subset of strategies used by other libraries (e.g., std::sscanf). Their purpose is to allow for the fastest possible implementation in common high-throughput environments, such as text-based exchanges (JSON or XML).

execution

This header is part of the algorithm library. It specifies the execution policy for standard library algorithms. The main execution policies include:

  • • Sequential execution (std::execution::seq)
  • • Parallel execution (std::execution::par)
  • • Parallel and vectorized execution (std::execution::par_unseq)
  • • Vectorized execution (std::execution::unseq) (introduced in C++20)

Using execution policies is very simple, typically as the first parameter of the algorithm. For example, sorting a vector:

#include <vector>
#include <algorithm>
#include <execution>

std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};

// Sequential execution
std::sort(std::execution::seq, v.begin(), v.end());

// Parallel execution
std::sort(std::execution::par, v.begin(), v.end());

// Parallel and vectorized execution
std::sort(std::execution::par_unseq, v.begin(), v.end());

Currently, most compilers have incomplete support for Parallel algorithms and execution policies, rendering them mostly unusable.

filesystem

Lazy to read

memory_resource

The standard library provides some memory pool resources, offering memory resources for containers via polymorphic_allocator, compared to std::pmr::vector:

namespace pmr {
  template<typename _Tp> class polymorphic_allocator;
  template<typename _Tp>
    using vector = std::vector<_Tp, polymorphic_allocator<_Tp>>;
} // namespace pmr

polymorphic_allocator is constructed with std::pmr::memory_resource* as a parameter, and memory pool resource classes inherit from std::pmr::memory_resource

int main()
{
    std::array<std::byte, 1024> buffer;
    // Create a monotonic_buffer_resource (a memory_resource that only releases allocated memory when the resource is destroyed) using this buffer
    std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};
    // Create a vector using this memory pool
    std::pmr::vector<int> vec(&pool);
    for (int i = 0; i < 100; ++i)
    {
        vec.push_back(i);
    }
    // When the pool goes out of scope and is destroyed, all memory will be released at once
    return 0;
}

optional

std::optional is a wrapper that may or may not hold an object

string_view

The template class std::basic_string_view provides a lightweight object that offers read-only access to a portion of a string using an interface similar to std::basic_string.

variant

std::variant is a “type-safe union” that holds a value of one of its type list at any given time and keeps track of the currently held type. Unlike C-style unions, variant can safely hold non-trivial types and construct/destruct objects directly within the union. Variant cannot directly hold references, arrays, or void; if a “null state” is needed, std::monostate can be used as one of the options.

  • • Default construction: defaults to the first option; if the first type cannot be default constructed, then the variant cannot be default constructed.
  • • Option construction:<span>std::variant<int, std::string> v1{123}, v2("hello");</span>
  • • In-place construction: using std::in_place_type as the first parameter allows for in-place construction,<span>std::variant<int, std::string> v{std::in_place_type<std::string>, "world"};</span>
std::vector<std::variant<int, double, std::string>> vec = {10, 1.5, "hello"};
for (const std::variant<int, double, std::string> &v : vec)
{
    std::visit([](const auto& arg){std::cout << arg;}, v);
}

It seems that auto is determined at runtime, but actually, the compiler generates code for the lambda like this:

struct lambda_0
{
    template <class T>
    void operator()(const T &arg) const;
};

For each type, an overload is generated, and std::visit jumps to the corresponding overload function based on the internal index of the variant using a compile-time generated jump table (similar to a switch statement).

std::tuple is a simple struct replacement, while std::variant is a union replacement.

New Library Features

Some Mathematical Functions

Function Name Header File Functionality
gcd numeric Greatest common divisor
lcm numeric Least common multiple
hypot cmath Hypotenuse (square root of the sum of squares)
cbrt cmath Cube root
remainder cmath Remainder of floating-point division
trunc cmath Nearest integer not greater than arg
nearbyint cmath Nearest integer to a floating-point number

std::apply and std::invoke

std::apply and std::make_from_tuple are located in the tuple header, while std::invoke is located in the functional header.

std::apply unpacks elements from a tuple or similar structure (types that support std::get and std::tuple_size, such as std::array and std::pair) as parameters and then calls a callable object (usually a function, lambda, function object, etc.), and can only be used for callable objects that accept parameters, not for directly calling member variables.

std::invoke calls callable objects (such as functions, member functions, member variables, function objects, lambdas, etc.) in a general way.

std::make_from_tuple can be thought of as a special case of std::apply optimized for constructing objects, only supporting tuples as parameters:

template <typename T, typename... Args>
constexpr T make_from_tuple1(const std::tuple<Args...> &t)
{
    return std::apply([](Args... args){ return T(args...); }, t);
}

std::not_fn

std::not_fn (in the functional header) is a function adapter introduced in C++17 that wraps a callable object into a new callable object that performs logical negation, replacing std::not1/std::not2 from C++03.

std::vector<int> v = {-3, -2, -1, 0, 1, 2, 3};
auto not3 = [](int x) { return x != 3; };
auto it = std::find_if(v.begin(), v.end(), std::not_fn(not3));
if (it != v.end())
    cout << *it;

Searchers

std::search has a new overload (in the algorithm header):

template <class ForwardIt, class Searcher>
ForwardIt search(ForwardIt first, ForwardIt last, const Searcher& searcher);

Searches for the pattern specified in the searcher constructor within the sequence [first, last), equivalent to executing return searcher(first, last).first;

The standard library provides 3 searchers (in the functional header):default_searcher, standard C++ library search algorithm implementationboyer_moore_searcher, Boyer-Moore search algorithm implementationboyer_moore_horspool_searcher, Boyer-Moore-Horspool search algorithm implementation

int main()
{
    std::string in = "Lorem ipsum dolor sit amet, consectetur adipiscing elit,"
                     " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua";
    std::string needle = "pisci";
    auto [it1, it2] = std::boyer_moore_searcher(needle.begin(), needle.end())(in.begin(), in.end());
    if (it1 != in.end())
    {
        std::cout << std::string(it1, it2) << "\n";
    }
    else
        std::cout << "The string " << needle << " not found\n";
}

std::byte

std::byte is a distinct type representing a byte, defined in cstddef.

std::byte b{42};
std::cout << std::to_integer<int>(b) << "\n";

conjunction/disjunction/negation

Located in the types header

std::clamp

std::clamp is used to “clamp” a value between specified lower and upper bounds, located in the algorithm header, possible implementation:

template <class T>
constexpr const T &clamp(const T &v, const T &low, const T &high)
{
    assert(!(high < low));
    return v < low ? low : (high < v ? high : v);
}

Container Library Related

Extract and Merge Methods in map/set

extract extracts a node from the container, which can be modified and inserted back into the original container or another container of the same type.

std::map<int, string> m{{1, "mango"}, {2, "papaya"}, {3, "guava"}};
auto nh = m.extract(2);
h.key() = 4;
m.insert(std::move(nh));
// m == {{1, "mango"}, {3, "guava"}, {4, "papaya"}}

merge merges all elements from the container pointed to by the parameter into the current container, retaining duplicates from the source container if there are any.

try_emplace and insert_or_assign Methods in map/unordered_map

try_emplace attempts to insert an element into the container; if the key already exists, no operation is performed.

insert_or_assign attempts to insert or replace an element in the container; if the key does not exist, it is inserted; if the key exists, the old value is replaced with the new value.

In most cases, insert_or_assign is similar to directly using operator[], but insert_or_assign does not require the mapped type to be default constructible.

Contiguous Iterators

Contiguous iterators represent elements that are stored in a contiguous area of memory in order, being a special case of random access iterators, thus possessing all the capabilities of random access, and additionally ensuring that &it can yield a pointer to the element, with pointer arithmetic being equivalent to iterator arithmetic.

std::vector<int> v = {10, 20, 30, 40, 50};
auto it = v.begin();
// Random access
std::cout << it[2] << '\n'; // 30
// Pointer semantics: &*it is equivalent to a pointer to the first element
int *p = &*it;
std::cout << p[2] << '\n'; // 30

Non-member Functions size/empty/data

Omitted

Leave a Comment