C++17

Egor Bredikhin
Articles: 4



C++ language is constantly evolving, and for us, as for developers of a static analyzer, it is important to track all its changes, in order to support all new features of the language. In this review article, I would like to share with the reader the most interesting innovations introduced in C++17, and demonstrate them with examples.

Рисунок 2

Now, developers of compilers are actively adding support for the new standard. You can see what is supported at the moment via the following links:

Fold expressions

I would like to start with a few words about what a fold is (also known as reduce or accumulate).

Fold is a function that applies the assigned combining function to sequential pairs of elements in a list, and returns a result. The simplest example is the summing up of elements in the list using a fold:

Example from C++:

std::vector<int> lst = { 1, 3, 5, 7 };
int res = std::accumulate(lst.begin(), lst.end(), 0, 
  [](int a, int b)  { return a + b; });
std::cout << res << '\n'; // 16

If the combining function is applied to the first item in a list and to the result of the recursive processing of the tail of a list, then the fold is called 'right'. In our example, we will get:

1 + (3 + (5 + (7 + 0)))

If the combining function is applied to the result of the recursive processing at the top of the list (the entire list without the last element) and to the last element, then a folding is called 'left'. In our example, we will get:

(((0 + 1) + 3) + 5) + 7

Thus, the fold type determines the order of evaluation.

In C++17 there is also folding support for a template parameters list. It has the following syntax:

(pack op ...) A unary right associative fold
(... op pack) A unary left associative fold
(pack op ... op init) A binary right associative fold
(init op ... op pack) A binary left associative fold

op is one of the following binary operators:

+ - * / % ^ & | ~ = < > << >> += -= *= /= %=
^= &= |= <<= >>= == != <= >= && || , .* ->*

pack is an expression containing an undisclosed parameter pack

init - initial value

For example, here's a template function that takes a variable number of parameters and calculates their sum:

// C++17
#include <iostream>

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

int main()
{
  std::cout << Sum(1, 2, 3, 4, 5) << '\n'; // 15
  return 0;
}

Note: In this example, the Sum function could be also declared as constexpr.

If we want to specify an initial value, we can use binary fold:

// C++17
#include <iostream>

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

int main()
{
  std::cout << Func(1, 2, 3, 4, 5) << '\n'; //115
  return 0;
}

Before C++17, to implement a similar function, you would have to explicitly specify the rules for recursion:

// C++14
#include <iostream>

auto Sum()
{
  return 0;
}

template<typename Arg, typename... Args>
auto Sum(Arg first, Args... rest)
{
  return first + Sum(rest...);
}

int main()
{
  std::cout << Sum(1, 2, 3, 4); // 10
  return 0;
}

It is worth highlighting the operator ',' (comma), which will expand the pack into a sequence of actions separated by commas. Example:

// C++17
#include <iostream>

template<typename T, typename... Args>
void PushToVector(std::vector<T>& v, Args&&... args)
{
  (v.push_back(std::forward<Args>(args)), ...);

//This code is expanded into a sequence of expressions      
//separated by commas as follows:
  //v.push_back(std::forward<Args_1>(arg1)),
  //v.push_back(std::forward<Args_2>(arg2)),
  //....
}

int main()
{
  std::vector<int> vct;
  PushToVector(vct, 1, 4, 5, 8);
  return 0;
}

Thus, folding greatly simplifies work with variadic templates.

template<auto>

Now you can use auto in templates for non-type template parameters. For example:

// C++17
template<auto n>
void Func() { /* .... */ }

int main()
{
  Func<42>(); // will issue int type
  Func<'c'>(); // will issue char type
  return 0;
}

Previously the only way to pass a non-template type parameter with an unknown type was a passing of two parameters: type and value. An example of this, would look as follows:

// C++14
template<typename Type, Type n>
void Func() { /* .... */ }

int main()
{
  Func<int, 42>();
  Func<char, 'c'>();
  return 0;
}

Class template argument deduction

Before C++17 a template argument deduction has only worked for the functions, and so, when constructing template class it has always been necessary to explicitly specify the template parameters:

// C++14
auto p = std::pair<int, char>(10, 'c');

or use specialized functions like std::make_pair for the implicit type deduction:

// C++14
auto p = std::make_pair(10, 'c');

This related to the fact that it was quite difficult to deduce a type when having several constructors in a class. In the new standard this problem has been solved:

#include <tuple>
#include <array>

template<typename T, typename U>
struct S
{
  T m_first;
  U m_second;
  S(T first, U second) : m_first(first), m_second(second) {}
};

int main()
{
  // C++14
  std::pair<char, int> p1 = { 'c', 42 };
  std::tuple<char, int, double> t1 = { 'c', 42, 3.14 };
  S<int, char> s1 = { 10, 'c' };

  // C++17
  std::pair p2 = { 'c', 42 };
  std::tuple t2 = { 'c', 42, 3.14 };
  S s2 = { 10, 'c' };

  return 0;
}

New standard defined a plenty of deduction guides. Also there is a possibility to write these guides ourselves, for instance:

// C++17
#include <iostream>

template<typename T, typename U>
struct S
{
  T m_first;
  U m_second;
};

// My deduction guide
template<typename T, typename U>
S(const T &first, const U &second) -> S<T, U>;

int main()
{
  S s = { 42, "hello" };
  std::cout << s.m_first << s.m_second << '\n';

  return 0;
}

A majority of standard containers work without the necessity to manually specify deduction guide.

Note: the compiler is able to create deduction guide automatically from a constructor, but in this example, the structure S has no constructor, so, we define deduction guide manually.

Thus, template argument deduction for classes allows us to significantly reduce code, and forget about special functions such as std::make_pair, std::make_tuple, and use the constructor instead.

Constexpr if

C++17 gives us the ability to perform compile-time conditional branching. This is a very powerful tool, particularly useful in metaprogramming. I will give a simple example:

// C++17
#include <iostream>
#include <type_traits>

template <typename T>
auto GetValue(T t)
{
  if constexpr (std::is_pointer<T>::value)
  {
    return *t;
  }
  else
  {
    return t;
  }
}

int main()
{
  int v = 10;
  std::cout << GetValue(v) << '\n'; // 10
  std::cout << GetValue(&v) << '\n'; // 10

  return 0;
}

Before C++17 we would have to use SFINAE and enable_if:

// C++14
template<typename T>
typename std::enable_if<std::is_pointer<T>::value,
  std::remove_pointer_t<T>>::type
GetValue(T t)
{
  return *t;
}

template<typename T>
typename std::enable_if<!std::is_pointer<T>::value, T>::type
GetValue(T t)
{
  return t;
}
int main()
{
  int v = 10;
  std::cout << GetValue(v) << '\n'; // 10
  std::cout << GetValue(&v) << '\n'; // 10

  return 0;
}

It is easy to see that code with constexpr if is much more readable.

Constexpr lambdas

Before C++17 lambdas were not compatible with constexpr. Now you can write lambdas inside constexpr expressions, and you can also declare lambdas themselves as constexpr.

Note: even if the constexpr specifier is omitted, the lambda will still be constexpr, if possible.

Example with lambda inside constexpr functions:

// C++17
constexpr int Func(int x)
{
  auto f = [x]() { return x * x; };
  return x + f();
}

int main()
{
  constexpr int v = Func(10);
  static_assert(v == 110);

  return 0;
}

Example with constexpr lambda:

// C++17
int main()
{
  constexpr auto squared = [](int x) { return x * x; };
  constexpr int s = squared(5);
  static_assert(s == 25);

  return 0;
}

*this capture in lambda expressions

Lambda expressions can now capture class members by value using *this:

class SomeClass
{
public:
  int m_x = 0;

  void f() const
  {
    std::cout << m_x << '\n';
  }

  void g()
  {
    m_x++;
  }

  // C++14
  void Func()
  {
    // const *this copy
    auto lambda1 = [self = *this](){ self.f(); };
    // non-const *this copy
    auto lambda2 = [self = *this]() mutable { self.g(); };
    lambda1();
    lambda2();
  }

  // C++17
  void FuncNew()
  {
    // const *this copy
    auto lambda1 = [*this](){ f(); }; 
    // non-const *this copy
    auto lambda2 = [*this]() mutable { g(); };
    lambda1();
    lambda2();
  }
};

inline variables

In C++17, in addition to inline functions, inline variables have been also introduced. A variable or a function, declared inline, can be defined (necessarily identically) in several translation units.

Inline variables can be useful for developers of libraries consisting of a single header file. Let me give you a small example:

(Instead of writing the extern and assigning the value in .cpp value)

header.h:

#ifndef _HEADER_H
#define _HEADER_H
inline int MyVar = 42;
#endif

source1.h:

#include "header.h"
....
MyVar += 10;

source2.h:

#include "header.h"
....
Func(MyVar);

Before C++17 a programmer would have to declare a MyVar variable as extern, and assign a value to it in one of the .cpp files.

Structured bindings

A convenient mechanism appeared for decomposition of objects such as, for example, pairs or tuples, which is called Structured bindings or Decomposition declaration.

I'll demonstrate it using an example:

// C++17
#include <set>

int main()
{
  std::set<int> mySet;
  auto[iter, ok] = mySet.insert(42);
  ....
  return 0;
}

The insert() method returns pair<iterator, bool>, where the iterator is the iterator to the inserted object, and bool is false if the element was not inserted (i.g. has already been contained in mySet).

Before C++17, a programmer would have to use std::tie:

// C++14
#include <set>
#include <tuple>

int main()
{
  std::set<int> mySet;
  std::set<int>::iterator iter;
  bool ok;
  std::tie(iter, ok) = mySet.insert(42);
  ....
  return 0;
}

The obvious disadvantage is that the variables iter and ok have to be pre-declared.

In addition, structured binding can be used with arrays:

// C++17
#include <iostream>

int main()
{
  int arr[] = { 1, 2, 3, 4 };
  auto[a, b, c, d] = arr;
  std::cout << a << b << c << d << '\n';

  return 0;
}

You can also implement a decomposition of types that contain only non-static public members.

// C++17
#include <iostream>

struct S
{
  char x{ 'c' };
  int y{ 42 };
  double z{ 3.14 };
};

int main()
{
  S s;
  auto[a, b, c] = s;
  std::cout << a << ' ' << b << ' ' << c << ' ' << '\n';

  return 0;
}

In my opinion, a very handy application of structured binding is its usage in range-based loops:

// C++17
#include <iostream>
#include <map>

int main()
{
  std::map<int, char> myMap;
  ....

  for (const auto &[key, value] : myMap)
  {
    std::cout << "key: " << key << ' ';
    std::cout << "value: " << value << '\n';
  }

  return 0;
}

Initializer in 'if' and 'switch'

'if' and 'switch' operators with the initializer appeared in C++17.

if (init; condition)
switch(init; condition)

Example of usage:

if (auto it = m.find(key); it != m.end())
{
  ....
}

They look very well in connection with a structured binding, mentioned above. For example:

std::map<int, std::string> myMap;
....
if (auto[it, ok] = myMap.insert({ 2, "hello" }); ok)
{
  ....
}

__has_include

The preprocessor's predicate __has_include allows to check if the header file is available for inclusion.

Here is an example directly from proposal for the standard (P0061R1). In this example we include 'optional' if it is available:

#if __has_include(<optional>)
  #include <optional>
  #define have_optional 1
#elif __has_include(<experimental/optional>)
  #include <experimental/optional>
  #define have_optional 1
  #define experimental_optional 1
#else
  #define have_optional 0
#endif

New attributes

In addition to the already existing standard attributes [noreturn]], [[carries_dependency]] and [[deprecated]], tree new attributes appeared in C++17:

[[fallthrough]]

This attribute indicates that the break operator inside a case block is missing intentionally (i.e., control is passed to the next case block), and therefore, a compiler or static code analyzer warning should not be issued.

Quick example:

// C++17
switch (i)
{
case 10:
  f1();
  break;
case 20:
  f2();
  break;
case 30:
  f3();
  break;
case 40:
  f4();
  [[fallthrough]]; // The warning will be suppressed
case 50:
  f5();
}

[[nodiscard]]

This attribute is used to indicate that the return value of the function should not be ignored:

// C++17
[[nodiscard]] int Sum(int a, int b)
{
  return a + b;
}

int main()
{
  Sum(5, 6); // Compiler/analyzer warning will be issued
  return 0;
}

[[nodiscard]] can be also applied to data types or enumerations to mark all functions that return this type as [[nodiscard]]:

// C++17
struct [[nodiscard]] NoDiscardType
{
  char a;
  int b;
};

NoDiscardType Func()
{
  return {'a', 42};
}

int main()
{
  Func(); // Compiler/analyzer warning will be issued
  
  return 0;
}

[[maybe_unused]]

This attribute is used to suppress compiler/analyzer warnings for unused variables, function parameters, static functions, and more.
Examples:

// The warning will be suppressed 
[[maybe_unused]] static void SomeUnusedFunc() { .... }

// The warning will be suppressed
void Foo([[maybe_unused]] int a) { .... }
void Func()
{
  // The warning will be suppressed
  [[maybe_unused]] int someUnusedVar = 42;
  ....
}

std: byte type

std::byte is suggested for use when working with 'raw' memory. Typically, for this char, unsigned char or uint8_t are used. std::byte type is more type-safe, since only bitwise operations can be applied to it, but arithmetic and implicit conversions are not available. In other words, a pointer to a std::byte will not be useable as an actual argument to the F(const unsigned char *) function call.

This new type is defined in <cstddef> as follows:

enum class byte : unsigned char {};

Dynamic memory allocation of over-aligned types

alignas specifier was added to C++11, allowing to manually specify alignment for a type or variable. Before C++17 there were no assurances that the alignment would be set in accordance with the alignas during dynamic memory allocation. Now, the new standard ensures that the alignment will be taken into account:

// C++17
struct alignas(32) S
{
  int a;
  char c;
};

int main()
{
  S *objects = new S[10];
  ....

  return 0;
}

More rigorous evaluation order of expressions

C++17 introduces new rules, defining more strictly the evaluation order of expressions:

  • Postfix expressions are evaluated from left to right (including function calls and access to objects members)
  • Assignment expressions are evaluated from right to left.
  • Operands of operators << and >> are evaluated from left to right.

Thus, as it is mentioned in the proposal for the standard, in the following expressions a is now guaranteed to be evaluated first, then b, then c, then d:

a.b
a->b
a->*b
a(b1, b2, b3)
b @= a
a[b]
a << b << c
a >> b >> c

Note that the evaluation order between b1, b2, b3 is still not defined.

Let me give you one good example from the proposal for the standard:

string s = 
  "but I have heard it works even if you don't believe in it";
s.replace(0, 4, "")
.replace(s.find("even"), 4, "only")
.replace(s.find(" don't"), 6, "");
assert(s == "I have heard it works only if you believe in it");

This is the code from a book of Stroustrup "The C++ Programming Language, 4th edition", which was used to demonstrate the methods call in a chain order. Previously, this code had unspecified behavior; starting with C++17 it will work as intended. The problem was that it was not clear which of the find functions would be called first.

So, now in expressions as these:

obj.F1(subexr1).F2(subexr2).F3(subexr3).F4(subexr4)

Subexpressions subexr1, subexr2, subexr3, subexr4 are evaluated in accordance to the order of calling the F1, F2, F3, F4 functions. Previously, the evaluation order of such expressions has not been defined, which lead to errors.

Filesystem

C++17 provides possibilities for cross-platform work with file system. This library is actually a boost::filesystem, which was moved to the standard with minor changes.

Let's see some examples of work with std::filesystem.

Header file and namespace:

#include <filesystem>
namespace fs = std::filesystem;

Work with a fs::path object:

fs::path file_path("/dir1/dir2/file.txt");
cout << file_path.parent_path() << '\n'; // It'll print "/dir1/dir2"
cout << file_path.filename() << '\n'; // It'll print "file.txt"
cout << file_path.extension() << '\n'; // It'll print ".txt"

file_path.replace_filename("file2.txt");
file_path.replace_extension(".cpp");
cout << file_path << '\n'; // It'll print "/dir1/dir2/file2.cpp"

fs::path dir_path("/dir1");
dir_path.append("dir2/file.txt");
cout << dir_path << '\n'; // It'll print "/dir1/dir2/file.txt"

Working with directories:

// Getting the current working directory
fs::path current_path = fs::current_path();

// Creating a directory
fs::create_directory("/dir");

// Creating several directories
fs::create_directories("/dir/subdir1/subdir2");

// Verifying the existence of a directory
if (fs::exists("/dir/subdir1"))
{
  cout << "yes\n";
}

// Non-recursive directory traversal
for (auto &p : fs::directory_iterator(current_path))
{
  cout << p.path() << '\n';
}

// Recursive directory traversal
for (auto &p : fs::recursive_directory_iterator(current_path))
{
  cout << p.path() << '\n';
}

// Nonrecursive directory copy
fs::copy("/dir", "/dir_copy");

// Recursive directory copy
fs::copy("/dir", "/dir_copy", fs::copy_options::recursive);

// Removal of the directory with all contents, if it exists
fs::remove_all("/dir");

The possible values of fs::copy_options, for processing already existing files, are presented in the table:

Constant Value
none If the file already exists, an exception is thrown. (The default value)
skip_existing Existing files are not overwritten, and an exception is not thrown.
overwrite_existing Existing files are overwritten.
update_existing Existing files are overwritten, only with newer files.

Working with files:

// Verifying the existence of a file
if (fs::exists("/dir/file.txt"))
{
  cout << "yes\n";
}

// Copying a file
fs::copy_file("/dir/file.txt", "/dir/file_copy.txt",
  fs::copy_options::overwrite_existing);

// Getting the file size (in bytes)
uintmax_t size = fs::file_size("/dir/file.txt");

// Renaming a file
fs::rename("/dir/file.txt", "/dir/file2.txt");

// Deleting a file if it exists
fs::remove("/dir/file2.txt");

This is not a complete list of std::filesystem abilities at all. All the features can be found here.

std::optional

This is a template class that stores an optional value. It is useful to, for example, return a value from a function in which an error can occur:

// C++17
std::optional<int> convert(my_data_type arg)
{
  ....
  if (!fail)
  {
    return result;
  }
  return {};
}

int main()
{
  auto val = convert(data);
  if (val.has_value())
  {
    std::cout << "conversion is ok, ";
    std::cout << "val = " << val.value() << '\n';
  }
  else
  {
    std::cout << "conversion failed\n";
  }

  return 0;
}

Also std::optional has value_or method, which returns a value from optional, if it is available or, otherwise, predefined value.

std::any

An object of std::any class can store any type of information. Thus, the same variable of std::any type can first store int, then float, and then a string. Example:

#include <string>
#include <any>

int main()
{
  std::any a = 42;
  a = 11.34f;
  a = std::string{ "hello" };
  return 0;
}

It is worth noting that std::any doesn't produce any type casting that will avoid ambiguity. For this reason, in the example std::string type is explicitly specified, otherwise in std::any object, a simple pointer will be stored.

To gain access to information stored in the std::any, you need to use std::any_cast. For example:

#include <iostream>
#include <string>
#include <any>
int main()
{
  std::any a = 42;
  std::cout << std::any_cast<int>(a) << '\n';

  a = 11.34f;
  std::cout << std::any_cast<float>(a) << '\n';

  a = std::string{ "hello" };
  std::cout << std::any_cast<std::string>(a) << '\n';

  return 0;
}

If the template parameter of std::any_cast is of any type, different from the type of the current stored object, an exception std::bad_any_cast would be thrown.

Information about a stored type can be gained using the method type():

#include <any>

int main()
{
  std::any a = 42;
  std::cout << a.type().name() << '\n'; // "int" will be displayed

  return 0;
}

std::variant

std::variant is a template class, which is the union, which remembers what type it stores. Also, unlike union, std::variant allows to store non-POD types.

#include <iostream>
#include <variant>

int main()
{
  // stores either int, or float or char.
  std::variant<int, float, char> v;
  v = 3.14f;
  v = 42;
  std::cout << std::get<int>(v);
  //std::cout << std::get<float>(v); // std::bad_variant_access
  //std::cout << std::get<char>(v); // std::bad_variant_access
  //std::cout << std::get<double>(v); // compile-error
  return 0;
}

To get values from std::variant a function std::get is used. It will throw an exception std::bad_variant_access, if one tries to take the wrong type.

There is also a std::get_if function, which takes a pointer to std::variant and returns a pointer to the current value, if the type was specified correctly, or, otherwise, nullptr:

#include <iostream>
#include <variant>

int main()
{
  std::variant<int, float, char> v;
  v = 42;
  auto ptr = std::get_if<int>(&v);
  if (ptr != nullptr)
  {
    std::cout << "int value: " << *ptr << '\n'; // int value: 42
  }

  return 0;
}

Usually a more convenient way of working with std::variant is std::visit:

#include <iostream>
#include <variant>

int main()
{
  std::variant<int, float, char> v;
  v = 42;

  std::visit([](auto& arg)
  {
    using Type = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<Type, int>)
    {
      std::cout << "int value: " << arg << '\n';
    }
    else if constexpr (std::is_same_v<Type, float>)
    {
      std::cout << "float value: " << arg << '\n';
    }
    else if constexpr (std::is_same_v<Type, char>)
    {
      std::cout << "char value: " << arg << '\n';
    }
  }, v);

  return 0;
}

std::string_view

In C++17 a special std::string_view class appeared, which stores a pointer to the beginning of an existing string and the size of this string. Thus, std::string_view can be treated as a string that doesn't own a memory.

std::string_view has constructors that take a std::string, char[N], char*, so there is no longer a necessity to write three overloaded functions:

// C++14
void Func(const char* str);
void Func(const char str[10]);
void Func(const std::string &str);

// C++17
void Func(std::string_view str);

Now, in all functions that take const std::string& as a parameter, the type can be changed to std::string_view because this will improve performance in cases when the string literal is passed into the function, or C-array. This is due to the fact that memory allocation usually occurs when constructing a std::string object, and when constructing std::string_view no allocations occur.

Changing the type of the const string& argument to string_view should not be performed, only in the case that inside this function the another function is called with this argument and receiving const string&.

try_emplace and insert_or_assign

In C++17 the containers std::map and std::unordered_map introduce new functions - try_emplace and insert_or_assign.

Unlike emplace, try_emplace function doesn't 'steal' move-only argument in a case where the insertion of the element didn't occur. The best way to explain this it is to give an example:

// C++17
#include <iostream>
#include <string>
#include <map>

int main()
{
  std::string s1("hello");
  std::map<int, std::string> myMap;
  myMap.emplace(1, "aaa");
  myMap.emplace(2, "bbb");
  myMap.emplace(3, "ccc");

  //std::cout << s1.empty() << '\n'; // 0
  //myMap.emplace(3, std::move(s1));
  //std::cout << s1.empty() << '\n'; // 1

  //std::cout << s1.empty() << '\n'; // 0
  //myMap.try_emplace(3, std::move(s1));
  //std::cout << s1.empty() << '\n'; // 0

  std::cout << s1.empty() << '\n'; // 0
  myMap.try_emplace(4, std::move(s1));
  std::cout << s1.empty() << '\n'; // 1

  return 0;
}

If the insertion does not occur, due to the fact that an element with the same key already exists in the myMap, try_emplace does not "steal" the string s1, unlike emplace.

The insert_or_assign function inserts the element in a container (if there is no element with such a key in a container) and rewrites the existing element, if the element with such key already exists. The function returns std::pair consisting of an iterator to the inserted/rewritten element, and a boolean value indicating whether the insertion of a new element occurred or not. Therefore, this function is similar to operator[], but it returns additional information based on whether the insertion or overwriting of the element was implemented:

// C++17
#include <iostream>
#include <string>
#include <map>

int main()
{
  std::map<int, std::string> m;
  m.emplace(1, "aaa");
  m.emplace(2, "bbb");
  m.emplace(3, "ccc");

  auto[it1, inserted1] = m.insert_or_assign(3, "ddd");
  std::cout << inserted1 << '\n'; // 0

  auto[it2, inserted2] = m.insert_or_assign(4, "eee");
  std::cout << inserted2 << '\n'; // 1

  return 0;
}

Before C++17, to figure out if the insert or update occurred, a programmer had to first look for the element, and then apply the operator[].

Special mathematical functions

In C++17, many specialized mathematical functions were added, such as: beta functions, the Riemann zeta function and others. You can read more about them here.

Declaration of nested namespaces

In C++17 you can write:

namespace ns1::ns2
{
  ....
}

Instead of:

namespace ns1
{
  namespace ns2
  {
    ....
  }
}

Non-constant string::data

In C++17 std::string has the data() method, which returns a non-constant pointer to internal string data:

// C++17
#include <iostream>

int main()
{
  std::string str = "hello";
  char *p = str.data();
  p[0] = 'H';
  std::cout << str << '\n'; // Hello

  return 0;
}

This will be useful when working with old C libraries.

Parallel algorithms

Functions from <algorithm>, working with containers, now have multithreaded versions. They were all given an additional overloading that takes execution policy as the first argument, which defines the way the algorithm will run.

Execution policy can be one of three values:

  • std::execution::seq - sequential execution
  • std::execution::par - parallel execution
  • std::execution::par_unseq - parallel vectorized execution

So, to get a multithreaded version of the algorithm, it is enough to write:

#include <iostream>
#include <vector>
#include <algorithm>
....
std::for_each(std::execution::par, vct.begin(), vct.end(),
  [](auto &e) { e += 42; });
....

It is necessary to keep track of the fact that the indirect expenses on creating threads did not outweigh the benefit of using multi-thread algorithms. Sure, a programmer also needs to check that there are no race conditions or deadlocks.

It is also worth noting the difference between std::execution::seq, and a version without such a parameter; if the execution policy is passed to the function, in this algorithm's exceptions that extend beyond the boundaries of the functor, it mustn't be thrown. If such an exception is thrown, std::terminate will be called.

Due to the addition of parallelism, several new algorithms have appeared:

std::reduce works the same way as std::accumulate, but the order is not rigorously defined, so it can work in parallel. It also has an overload that accepts the execution policy. A small example:

....
// Summing up all the vct elements in the parallel mode
std::reduce(std::execution::par, vct.begin(), vct.end())
....

std::transform_reduce applies the specified functor on the elements of a container, and then uses std::reduce.

std::for_each_n works similar to std::for_each, but a specified functor is applied only to the n elements. For example:

....
std::vector<int> vct = { 1, 2, 3, 4, 5 };
std::for_each_n(vct.begin(), 3, [](auto &e) { e += 10; });
// vct: {10, 20, 30, 4, 5}
....

std::invoke, trait is_invocable

std::invoke takes an entity that can be called, and a set of arguments; and calls this entity with these arguments. Such entities, for example, are a pointer to a function object with operator(), lambda-function and others:

// C++17
#include <iostream>
#include <functional>

int Func(int a, int b)
{
  return a + b;
}

struct S
{
  void operator() (int a)
  {
    std::cout << a << '\n';
  }
};

int main()
{
  std::cout << std::invoke(Func, 10, 20) << '\n'; // 30
  std::invoke(S(), 42); // 42
  std::invoke([]() { std::cout << "hello\n"; }); // hello

  return 0;
}

std::invoke can be of service to any template magic. Also in C++17 a trait std::is_invocable was added:

// C++17
#include <iostream>
#include <type_traits>

void Func() { };

int main()
{
  std::cout << std::is_invocable<decltype(Func)>::value << '\n'; // 1
  std::cout << std::is_invocable<int>::value << '\n'; // 0

  return 0;
}

std::to_chars, std::from_chars

New functions std::to_chars and std::from_chars appeared in C++17 for fast conversion numbers to strings and strings to numbers, respectively. Unlike other formatting functions from C and C++, std::to_chars does not depend on the locale, does not allocate memory, and does not throw exceptions; and it is aimed to provide maximum performance:

// C++17
#include <iostream>
#include <charconv>

int main()
{
  char arr[128];
  auto res1 = std::to_chars(std::begin(arr), std::end(arr), 3.14f);
  if (res1.ec != std::errc::value_too_large)
  {
    std::cout << arr << '\n';
  }

  float val;
  auto res2 = std::from_chars(std::begin(arr), std::end(arr), val);
  if (res2.ec != std::errc::invalid_argument &&
      res2.ec != std::errc::result_out_of_range)
  {
    std::cout << arr << '\n';
  }

  return 0;
}

std::to_chars function returns a to_chars_result structure:

struct to_chars_result
{
  char* ptr;
  std::errc ec;
};

ptr is a pointer to the last written character + 1

ec is error code

std::from_chars function returns a from_chars_result structure:

struct from_chars_result 
{
  const char* ptr;
  std::errc ec;
};

ptr is a pointer to the first character that is not satisfying pattern

ec is error code

In my opinion, you should use these functions anywhere where conversion from a string to a number and from number to string is needed, in cases when you have just enough of C-locale, because it will provide good performance improvement.

std::as_const

The helper function std::as_const receives a reference and returns a reference to a constant:

// C++17
#include <utility>
....
MyObject obj{ 42 };
const MyObject& constView = std::as_const(obj);
....

Free functions std::size, std::data and std::empty

In addition to the already existing free functions std::begin, std::end and others, some new free functions appeared, such as: std::size, std::data and std::empty:

// C++17
#include <vector>

int main()
{
  std::vector<int> vct = { 3, 2, 5, 1, 7, 6 };

  size_t sz = std::size(vct);
  bool empty = std::empty(vct);
  auto ptr = std::data(vct);

  int a1[] = { 1, 2, 3, 4, 5, 6 };

  // should be used for C-style arrays.
  size_t sz2 = std::size(a1);
  return 0;
}

std::clamp

In C++17, the new std::clamp(x, low, high) function appeared, which returns x if it is in the interval [low, high] or, otherwise, the nearest value:

// C++17
#include <iostream>
#include <algorithm>

int main()
{
  std::cout << std::clamp(7, 0, 10) << '\n'; // 7
  std::cout << std::clamp(7, 0, 5) << '\n'; //5
  std::cout << std::clamp(7, 10, 50) << '\n'; //10

  return 0;
}

GCD and LCM

The Greatest Common Divisor (std::gcd) and Lowest Common Multiple (std::lcm) computation appeared in the standard:

// C++17
#include <iostream>
#include <numeric>

int main()
{
  std::cout << std::gcd(24, 60) << '\n'; // 12
  std::cout << std::lcm(8, 10) << '\n'; // 40

  return 0;
}

Logical operation metafunctions

In C++17, std::conjunction, std::disjunction and std::negation logical metafunctions appeared. They are used to perform a logical AND, OR, and NOT on a set of traits, respectively. A small example with std::conjunction:

// C++17
#include <iostream>
#include <string>
#include <algorithm>
#include <functional>

template<typename... Args>
std::enable_if_t<std::conjunction_v<std::is_integral<Args>...>>
Func(Args... args)
{
  std::cout << "All types are integral.\n";
}

template<typename... Args>
std::enable_if_t<!std::conjunction_v<std::is_integral<Args>...>>
Func(Args... args)
{
  std::cout << "Not all types are integral.\n";
}

int main()
{
  Func(42, true); // All types are integral.
  Func(42, "hello"); // Not all types are integral. 

  return 0;
}

I'd like to note that unlike template parameters folding mentioned above, the functions of std::conjunction and std::disjunction will stop instantiation once the resulting value can determined.

Attributes in namespaces and enums

Now you can use the attributes for namespaces and for enums, as well as within them:

// C++17
#include <iostream>

enum E
{
  A = 0,
  B = 1,
  C = 2,
  First[[deprecated]] = A,
};

namespace[[deprecated]] DeprecatedFeatures
{
  void OldFunc() {};
//....
}

int main()
{
  // Compiler warning will be issued
  DeprecatedFeatures::OldFunc();
  
  // Compiler warning will be issued
  std::cout << E::First << '\n'; 

  return 0;
}

Using prefix for attributes

Using prefix has been added for attributes, so if you are using multiple attributes, you can slightly reduce the amount code. Example from the proposal for the standard (P0028R4):

// C++14
void f() 
{
  [[rpr::kernel, rpr::target(cpu, gpu)]]
  task();
}

// C++17
void f() 
{
  [[using rpr:kernel, target(cpu, gpu)]]
  task();
}

The return value from emplace_back

emplace_back now returns a reference to the inserted element; before C++17, it did not return any value:

#include <iostream>
#include <vector>

int main()
{
  std::vector<int> vct = { 1, 2, 3 };

  auto &r = vct.emplace_back(10);
  r = 42;

  for (const auto &i : vct)
  {
    std::cout << i << ' ';
  }
}

Functors for searching for substring in string (Searcher functors)

In C++17, there are now functors which implement a search for a substring in a string, using the Boyer-Moore algorithm or Boyer-Moore-Horspul algorithm. These functors can be passed to std::search:

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>

int main()
{
  std::string haystack = "Hello, world!";
  std::string needle = "world";

  // Standard search
  auto it1 = std::search(haystack.begin(), haystack.end(),
    needle.begin(), needle.end());

  auto it2 = std::search(haystack.begin(), haystack.end(),
    std::default_searcher(needle.begin(), needle.end()));

  // Search using the Boyer-Moore algorithm
  auto it3 = std::search(haystack.begin(), haystack.end(),
    std::boyer_moore_searcher(needle.begin(), needle.end()));

  // Search using the Boyer-Moore algorithm-Horspula
  auto it4 = std::search(haystack.begin(), haystack.end(),
    std::boyer_moore_horspool_searcher(needle.begin(), needle.end()));

  std::cout << it1 - haystack.begin() << '\n'; // 7
  std::cout << it2 - haystack.begin() << '\n'; // 7
  std::cout << it3 - haystack.begin() << '\n'; // 7
  std::cout << it4 - haystack.begin() << '\n'; // 7

  return 0;
}

std::apply

std::apply calls callable-object with a set of parameters, stored in a tuple. Example:

#include <iostream>
#include <tuple>

void Func(char x, int y, double z)
{
  std::cout << x << y << z << '\n';
}

int main()
{
  std::tuple args{ 'c', 42, 3.14 };
  std::apply(Func, args);

  return 0;
}

Constructing objects from tuples (std::make_from_tuple)

In C++17, there is now the ability to construct an object, by passing a set of arguments in the constructor, recorded in the tuple. To do this, the function std::make_from_tuple is used:

#include <iostream>
#include <tuple>

struct S
{
  char m_x;
  int m_y;
  double m_z;
  S(char x, int y, double z) : m_x(x), m_y(y), m_z(z) {}
};

int main()
{
  std::tuple args{ 'c', 42, 3.14 };
  S s = std::make_from_tuple<S>(args);
  std::cout << s.m_x << s.m_y << s.m_z << '\n';

  return 0;
}

std::not_fn (Universal negator not_fn)

In C++17, there is now a std::not_fn function that returns a predicate-negation. This function is intended to replace std::not1 and std::not2:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>

bool LessThan10(int a)
{
  return a < 10;
}

int main()
{
  std::vector vct = { 1, 6, 3, 8, 14, 42, 2 };

  auto n = std::count_if(vct.begin(), vct.end(),
    std::not_fn(LessThan10)); 
 
  std::cout << n << '\n'; // 2

  return 0;
}

Access to containers nodes (Node handle)

In C++17, you can now move a node directly from one container to another. There is no additional allocations or copying occur. Let me give you a small example:

// C++17
#include <map>
#include <string>

int main()
{
  std::map<int, std::string> myMap1{ { 1, "aa" },
{ 2, "bb" },
{ 3, "cc" } };
  std::map<int, std::string> myMap2{ { 4, "dd" },
{ 5, "ee" },
{ 6, "ff" } };
  auto node = myMap1.extract(2);
  myMap2.insert(std::move(node));
 
  // myMap1: {{1, "aa"}, {3, "cc"}}
  // myMap2: {{2, "bb"}, {4, "dd"}, {5, "ee"}, {6, "ff"}}

  return 0;
}

std::extract method allows you to extract the node from the container, and the insert method is now able to insert nodes.

Also in C++17, containers have the merge method, which attempts to retrieve all nodes of the container using the extract, and insert them into another container using the insert:

// C++17
#include <map>
#include <string>

int main()
{
  std::map<int, std::string> myMap1{ { 1, "aa" },
{ 2, "bb" },
{ 3, "cc" } };
                                     
  std::map<int, std::string> myMap2{ { 4, "dd" },
{ 5, "ee" },
{ 6, "ff" } };
  myMap1.merge(myMap2);
  // myMap1: {{1, "aaa"},
{2, "bb"},
{3, "ccc"},
{4, "dd"},
{5, "ee"},
{6, "ff"}}
  // myMap2: {}

  return 0;
}

Another interesting example is the change of the element key in std::map:

// C++17
#include <map>
#include <string>

int main()
{
  std::map<int, std::string> myMap{ { 1, "Tommy" },
{ 2, "Peter" },
{ 3, "Andrew" } };
  auto node = myMap.extract(2);
  node.key() = 42;
  myMap.insert(std::move(node));

  // myMap: {{1, "Tommy"}, {42, "Peter"}, {3, "Andrew"}};

  return 0;
}

Before C++17, it wasn't possible to avoid additional overheads when changing a key.

static_assert with one argument

Now for the static_assert you do not need to specify the message:

static_assert(a == 42, "a must be equal to 42");
static_assert(a == 42); // Now you write like this
static_assert ( constant-expression ) ;
static_assert ( constant-expression , string-literal ) ;

std::*_v<T...>

In C++17, all traits from <type_traits> which have a field ::value, now have overloads like some_trait_v<T>. So now, instead of writing some_trait<T>::value, you can simply write some_trait_v<T>. For example:

// C++14
static_assert(std::is_integral<T>::value, "Integral required.");

// C++17
static_assert(std::is_integral_v<T>, "Integral required");

std::shared_ptr for arrays

shared_ptr now supports C-arrays. You only need to pass T[] as a templated parameter and shared_ptr will call delete[] when freeing memory. Previously, for arrays it was necessary to specify a function for manual deletion. A small example:

#include <iostream>
#include <memory>

int main()
{
  // C++14
  //std::shared_ptr<int[]> arr(new int[7],
  //  std::default_delete<int[]>());

  // C++17
  std::shared_ptr<int[]> arr(new int[7]);

  arr.get()[0] = 1;
  arr.get()[1] = 2;
  arr.get()[2] = 3;
  ....

  return 0;
}

std::scoped_lock

In C++17, there is now a new class scoped_lock, which blocks a few mutexes simultaneously (using lock) during creation and frees them all in the destructor, providing a convenient RAII-interface. A small example:

#include <thread>
#include <mutex>
#include <iostream>

int var;
std::mutex varMtx;

void ThreadFunc()
{
  std::scoped_lock lck { varMtx };
  var++;
  std::cout << std::this_thread::get_id() << ": " << var << '\n';
} // <= varMtx automatically frees when exiting block

int main()
{
  std::thread t1(ThreadFunc);
  std::thread t2(ThreadFunc);

  t1.join();
  t2.join();

  return 0;
}

Remote possibilities

  • Trigraphs have been removed.
  • The register keyword cannot be used as a variable specifier. It remains reserved for the future as it has been with auto.
  • Prefix and postfix increments for a bool type have been removed.
  • Exception specification has been removed. You cannot specify any more, what exceptions a function throws. In C++17 you can only mark functions that do not throw exceptions as noexcept.
  • std::auto_ptr was removed. We should use std::unique_ptr instead.
  • std::random_shuffle was removed. We should use std::shuffle instead with an appropriate functor, generating random numbers. A removal is related to the fact that std::random_shuffle used std::rand, which, in its turn, is considered to be deprecated.

Conclusions

Unfortunately, all the modules, concepts, networking, reflection, and other important features expected by everyone, were not included in C++17, so we look forward to C++20.

Myself, as one of the developers of PVS-Studio code analyzer, can point out that we have a lot of interesting work ahead. New language features are opening up new opportunities to "shoot yourself in the foot", and we must improve the analyzer to warn the programmer about potential new errors. For example, since C++14 it is possible to initialize a dynamic array when creating it. Therefore, it is useful to warn the programmer when the size of the dynamic array can be less than the number of elements in its initializer. This is why we have created a new diagnostic; V798. We have been, and will continue, doing diagnostics for new language constructions. For C++17 it would be useful, for example, to warn that in the algorithm for std::execution::par such constructions are used that can throw exceptions, and these exceptions wouldn't be specifically caught inside the algorithm using try...catch.

Thank you for your attention. I suggest you download PVS-Studio (Windows/Linux), and check your projects. The C++ language is becoming "bigger", and it is becoming more difficult to track down all the aspects and nuances of its use, to write correct code. PVS-Studio includes a large knowledge base of "Don'ts" and it will be an indispensable assistant to you. Besides, no one is insured from simple typos, and this problem will not go away. Proof.

Additional Links



Use PVS-Studio to search for bugs in C, C++, and C# code

We offer you to check your project code with PVS-Studio. Just one bug found in the project will show you the benefits of the static code analysis methodology better than a dozen of the articles.

goto PVS-Studio;

Egor Bredikhin
Articles: 4


Do you make errors in the code?

Check your code
with PVS-Studio

Static code analysis
for C, C++, and C#

goto PVS-Studio;