Skip to content

C++17 New Features and Libraries

New Features

Template Argument Deduction for Class Templates

Automatic template argument deduction much like how it's done for functions, but now including class constructors.

1
2
3
4
5
6
7
8
9
template <typename T = float>
struct MyContainer {
  T val;
  MyContainer() : val{} {}
  MyContainer(T val) : val{val} {}
  // ...
};
MyContainer c1 {1}; // OK MyContainer<int>
MyContainer c2; // OK MyContainer<float>

Declaring Non-type Template Parameters with auto

Following the deduction rules of auto, while respecting the non-type template parameter list of allowable types[*], template arguments can be deduced from the type ot its arguments:

1
2
3
4
5
6
7
8
9
template <auto... seq>
struct my_integer_sequence {
  // Implementation here ...
};

// Explicitly pass type `int` as template argument.
auto seq = std::integer_sequence<int, 0, 1, 2>();
// Type is deduced to be `int`.
auto seq2 = my_integer_sequence<0, 1, 2>();

Folding Expressions

A fold expression performs a fold of a template parameter pack over a binary operator.

  • An expression of the form (... op e) or (e op ...), where op is a fold-operator and e is an unexpanded parameter pack, are called unary folds.
  • An expression of the form (e1 op ... op e2), where op are fold-operators, is called a binary fold. Either e1 or e2 is an unexpanded parameter pack, but not both.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
template <typename... Args>
bool logicalAnd(Args... args) {
    // Binary folding.
    return (true && ... && args);
}
bool b = true;
bool& b2 = b;
logicalAnd(b, b2, true); // == true

template <typename... Args>
auto sum(Args... args) {
    // Unary folding.
    return (... + args);
}
sum(1.0, 2.0f, 3); // == 6.0

New Rules for auto Deduction from Braced-init-list

Changes to auto deduction when used with the uniform initialization syntax. Previously, auto x {3} deduced a std::initializer_list<int>, which now deduces to int.

1
2
3
4
auto x1 {1, 2, 3};  // error: not a single element.
auto x2 = {1, 2, 3};  // x2 is std::initializer_list<int>
auto x3 {3};  // x3 is int
auto x4 {3.0};  // x4 is double

constexpr Lambda

Compile-time lambda using constexpr.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
auto identity = [](int n) constexpr { return n; };
static_assert(identity(123) == 123);
constexpr auto add = [](int x, int y) {
  auto L = [=] { return x; };
  auto R = [=] { return y; };
  return [=] { return L() + R(); };
};

static_assert(add(1, 2)() == 3);
constexpr int addOne(int n) {
  return [n] { return n + 1; }();
}

static_assert(addOne(1) == 2);

Lambda Capture this by Value

Capturing this in a lambda's environment was previously reference-only. An example of where this is problematic is asynchronous code using callbacks that require an object to be available, potentially past its lifetime. *this (C++17) will now make a copy of the current object, while this (C++11) continues to capture by reference.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct MyObj {
  int value {123};
  auto getValueCopy() {
    return [*this] { return value; };
  }
  auto getValueRef() {
    return [this] { return value; };
  }
};
MyObj mo;
auto valueCopy = mo.getValueCopy();
auto valueRef = mo.getValueRef();
mo.value = 321;
valueCopy(); // 123
valueRef(); // 321

Inline Variables

The inline specifier can be applied to variables as well as to functions. A variable declared inline has the same semantics as a function declared inline.

1
2
3
4
5
6
7
8
// Disassembly example using compiler explorer.
struct S { int x; };
inline S x1 = S{321}; // mov esi, dword ptr [x1]
                      // x1: .long 321

S x2 = S{123};        // mov eax, dword ptr [.L_ZZ4mainE2x2]
                      // mov dword ptr [rbp - 8], eax
                      // .L_ZZ4mainE2x2: .long 123
It can also be used to declare and define a static member variable, such that it does not need to be initialized in the source file.
1
2
3
4
5
6
struct S {
  S() : id{count++} {}
  ~S() { count--; }
  int id;
  static inline int count{0}; // declare and initialize count to 0 within the class
};

Nested Namespaces

Using the namespace resolution operator to create nested namespace definitions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
namespace A {
  namespace B {
    namespace C {
      int i;
    }
  }
}
// vs.
namespace A::B::C {
  int i;
}

Structured Bindings

A proposal for de-structuring initialization, that would allow writting auto [ x, y, z] = expr; where the type of expr was a tuple-like object, whose elements would be bound to the variables x, y and z(which is construc declares). tuple-like objects include std::tuple, std::pair, std::array, and aggregate structures.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
using Coordinate = std::pair<int, int>;
Coordinate origin() {
  return Coordinate{0, 0};
}

const auto [ x, y ] = origin();
x; // == 0
y; // == 0
std::unordered_map<std::string, int> mapping {
  {"a", 1},
  {"b", 2},
  {"c", 3}
};

// Destructure by reference.
for (const auto& [key, value] : mapping) {
  // Do something with key and value
}

Selection Statements with Initializer

New version of if and switch statements which simplify code patterns and help users keep scopes tight.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{
  std::lock_guard<std::mutex> lk(mx);
  if (v.empty()) v.push_back(val);
}
// vs.
if (std::lock_guard<std::mutex> lk(mx); v.empty()) {
  v.push_back(val);
}

Foo gadget(args);
switch (auto s = gadget.status()) {
  case OK: gadget.zip(); break;
  case Bad: throw BadFoo(s.message());
}
// vs.
switch (Foo gadget(args); auto s = gadget.status()) {
  case OK: gadget.zip(); break;
  case Bad: throw BadFoo(s.message());
}

constexpr if

Write code that is instantiated depending on a compile=time condition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
template <typename T>
constexpr bool isIntegral() {
  if constexpr (std::is_integral<T>::value) {
    return true;
  } else {
    return false;
  }
}
static_assert(isIntegral<int>() == true);
static_assert(isIntegral<char>() == true);
static_assert(isIntegral<double>() == false);
struct S {};
static_assert(isIntegral<S>() == false);

UTF-8 Character Literals

A character literal that begins with u8 is a character literal of type char. The value of a UTF-8 character literal is equal to its ISO 10646 code point value.

1
char x = u8'x';

Direct List Initialization of Enums

Enums can now be initialized using braced syntax.

1
2
3
4
5
enum byte : unsigned char {};
byte b {0}; // OK
byte c {-1}; // ERROR
byte d = byte{1}; // OK
byte e = byte{256}; // ERROR

fallthrough, nodiscard, maybe_unused attributes

C++17 introduces threee new attributes:

  • [[fallthrough]]: indicates to the compiler that falling through in a switch statement is intended behavior.

    1
    2
    3
    4
    5
    6
    7
    switch (n) {
      case 1: [[fallthrough]]
        // ...
      case 2:
        // ...
        break;
    }
    

  • [[nodiscard]]: issues a warning when either a function or class has this attribute and its return value is discarded.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    [[nodiscard]] bool do_something() {
      return is_success; // true for success, false for failure
    }
    
    do_something(); // warning: ignoring return value of 'bool do_something()',
                    // declared with attribute 'nodiscard'
    
    // Only issues a warning when `error_info` is returned by value.
    struct [[nodiscard]] error_info {
      // ...
    };
    
    error_info do_something() {
      error_info ei;
      // ...
      return ei;
    }
    
    do_something(); // warning: ignoring returned value of type 'error_info',
                    // declared with attribute 'nodiscard'
    

  • [[maybe_unused]]: indicates to be compiler that a variable or parameter might be unused an is intended.

    1
    2
    3
    4
    void my_callback(std::string msg, [[maybe_unused]] bool error) {
      // Don't care if `msg` is an error message, just log it.
      log(msg);
    }
    

New Libraries

std::variant

The class template std::variant represents a type-safe union. An instance of std::variant at any given time holds a value of one of its alternativqe types(it's possible for it to be valueless).

1
2
3
4
5
6
std::variant<int, double> v{ 12 };
std::get<int>(v); // == 12
std::get<0>(v); // == 12
v = 12.0;
std::get<double>(v); // == 12.0
std::get<1>(v); // == 12.0

std::optional

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
std::optional<std::string> create(bool b) {
  if (b) {
    return "Godzilla";
  } else {
    return {};
  }
}

create(false).value_or("empty"); // == "empty"
create(true).value(); // == "Godzilla"
// optional-returning factory functions are usable as conditions of while and if
if (auto str = create(true)) {
  // ...
}

std::any

A type-safe container for single values of any type.

1
2
3
4
5
std::any x {5};
x.has_value() // == true
std::any_cast<int>(x) // == 5
std::any_cast<int&>(x) = 10;
std::any_cast<int>(x) // == 10

std::string_view

A non-owning reference to a string. Useful for providing an abstraction on top of strings (e.g. for parsing).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Regular strings.
std::string_view cppstr {"foo"};
// Wide strings.
std::wstring_view wcstr_v {L"baz"};
// Character arrays.
char array[3] = {'b', 'a', 'r'};
std::string_view array_v(array, std::size(array));

std::string str {"   trim me"};
std::string_view v {str};
v.remove_prefix(std::min(v.find_first_not_of(" "), v.size()));
str; //  == "   trim me"
v; // == "trim me"

std::invoke

Invoke a Callable object with parameters. Examples of Callable objects are std::function or std::bind where an object can be called similarly to a regular function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
template <typename Callable>
class Proxy {
  Callable c;
public:
  Proxy(Callable c): c(c) {}
  template <class... Args>
  decltype(auto) operator()(Args&&... args) {
    // ...
    return std::invoke(c, std::forward<Args>(args)...);
  }
};
auto add = [](int x, int y) {
  return x + y;
};
Proxy<decltype(add)> p {add};
p(1, 2); // == 3

std::apply

Invoke a Callable object with a tuple of arguments

1
2
3
4
auto add = [](int x, int y) {
  return x + y;
};
std::apply(add, std::make_tuple(1, 2)); // == 3

std::filesystem

The new std::filesystem library provides a standard way to manipulate files, directories, and paths in a filesystem.

1
2
3
4
5
6
7
8
9
const auto bigFilePath {"bigFileToCopy"};
if (std::filesystem::exists(bigFilePath)) {
  const auto bigFileSize {std::filesystem::file_size(bigFilePath)};
  std::filesystem::path tmpPath {"/tmp"};
  if (std::filesystem::space(tmpPath).available > bigFileSize) {
    std::filesystem::create_directory(tmpPath.append("example"));
    std::filesystem::copy_file(bigFilePath, tmpPath.append("newFile"));
  }
}

std::byte

The new std::byte type provides a standard way of representing data as byte. Benefits of using std::byte over char or unsigned char is that it is not a character type, and is also not an arithmetic type; while the only operator overloads available are bitwise operator.

1
2
3
4
5
std::byte a {0};
std::byte b {0xFF};
int i = std::to_integer<int>(b); // 0xFF
std::byte c = a & b;
int j = std::to_integer<int>(c); // 0

Splicing for Maps and Sets

Moving nodes and merging containers whithout the overhead of expensive copies, moves, or heap allocations/deallocations. Moving elements from one map to another:

1
2
3
4
5
std::map<int, string> src {{1, "one"}, {2, "two"}, {3, "buckle my shoe"}};
std::map<int, string> dst {{3, "three"}};
dst.insert(src.extract(src.find(1))); // Cheap remove and insert of { 1, "one" } from `src` to `dst`.
dst.insert(src.extract(2)); // Cheap remove and insert of { 2, "two" } from `src` to `dst`.
// dst == { { 1, "one" }, { 2, "two" }, { 3, "three" } };
Inserting elements which outlive the container:
1
2
3
4
5
6
auto elementFactory() {
  std::set<...> s;
  s.emplace(...);
  return s.extract(s.begin());
}
s2.insert(elementFactory());
Changing the key of a map element:
1
2
3
4
5
std::map<int, string> m {{1, "one"}, {2, "two"}, {3, "three"}};
auto e = m.extract(2);
e.key() = 4;
m.insert(std::move(e));
// m == { { 1, "one" }, { 3, "three" }, { 4, "two" } }

Parallel Algorithms

Many of the STL algorithms, such as the copy, find and sort methods, started to support the parallel execution policies: seq, par and par_unseq which translate to "sequentially", "parallel" and "parallel unsequenced".

1
2
3
4
5
std::vector<int> longVector;
// Find element using parallel execution policy
auto result1 = std::find(std::execution::par, std::begin(longVector), std::end(longVector), 2);
// Sort elements using sequential execution policy
auto result2 = std::sort(std::execution::seq, std::begin(longVector), std::end(longVector));