CPP17 in Detail Notes - yszheda/wiki GitHub Wiki

Part 1 - The Language Features

1. Fixes and Deprecation

Fixes

New auto rules for direct-list-initialisation

For the direct initialisation, C++17 introduces new rules:

  • For a braced-init-list with only a single element, auto deduction will deduce from that entry;
  • For a braced-init-list with more than one element, auto deduction will be ill-formed.
auto x1 = { 1, 2 }; // decltype(x1) is std::initializer_list<int>
auto x2 = { 1, 2.0 }; // error: cannot deduce element type
auto x3{ 1, 2 }; // error: not a single element
auto x4 = { 3 }; // decltype(x4) is std::initializer_list<int>
auto x5{ 3 }; // decltype(x5) is int

static_assert With no Message

Different begin and end Types in Range-Based For Loop

2. Language Clarification

Stricter Expression Evaluation Order

Now, with C++17, function chaining will work as expected when they contain inner expressions, i.e., they are evaluated from left to right: In an expression:

a(expA).b(expB).c(expC)

expA is evaluated before calling b.

the following expressions are evaluated in the order a, then b:

  1. a.b
  2. a->b
  3. a->*b
  4. a(b1, b2, b3) // b1, b2, b3 - in any order
  5. b @= a // '@' means any operator
  6. a[b]
  7. a << b
  8. a >> b

Guaranteed Copy Elision

  • Return Value Optimisation - RVO (disable RVO for gcc: -fno-elide-constructors)
  • Named Return Value Optimisation - NRVO

Currently, the standard allows eliding in cases like:

  • when a temporary object is used to initialise another object (including the object returned by a function, or the exception object created by a throw-expression)
  • when a variable that is about to go out of scope is returned or thrown
  • when an exception is caught by value

Why might this be useful?

  • to allow returning objects that are not movable/copyable - because we could now skip copy/move constructors
  • to improve code portability - as every conformant compiler supports the same rule
  • to support the ‘return by value’ pattern rather than using output arguments
  • to improve performance

In C++17 copy elision works only for temporary objects, not for Named RVO.

Updated Value Categories

Value Categories

  • lvalue - an expression that has an identity, and which we can take the address of
  • xvalue - “eXpiring lvalue” - an object that we can move from, which we can reuse. Usually, its lifetime ends soon
  • prvalue - pure rvalue - something without a name, which we cannot take the address of, we can move from such expression
  • glvalue - “generalised” lvalue - A glvalue is an expression whose evaluation computes the location of an object, bit-field, or function
  • prvalue - “pure” rvalue - A prvalue is an expression whose evaluation initialises an object, bit-field, or operand of an operator, as specified by the context in which it appears

In short: prvalues perform initialisation, glvalues describe locations.

The C++17 Standard specifies that when there’s a prvalue of some class or array there’s no need to create any temporary copies, the initialisation from that prvalue can happen directly. There’s no move or copy involved (so there’s no need to have a copy and move constructors); the compiler can safely do the elision.

It can happen:

  • in initialisation of an object from a prvalue: Type t = T()
  • in a function call where the function returns a prvalue.

There are several exceptions where the temporary is still needed:

  • when a prvalue is bound to a reference
  • when member access is performed on a class prvalue
  • when array subscripting is performed on an array prvalue
  • when an array prvalue is decayed to a pointer
  • when a derived-to-base conversions performed on a class prvalue
  • when a prvalue is used as a discarded value expression

Dynamic Memory Allocation for Over-Aligned Data

Exception Specifications as Part of the Type System

void (*p)();
void (**pp)() noexcept = &p; // error: cannot convert to
                             // pointer to noexcept function

struct S { typedef void (*p)(); operator p(); };
void (*q)() noexcept = S(); // error: cannot convert to
                            // pointer to noexcept

3. General Language Features

Structured Binding Declarations

// before C++17
std::set<int> mySet;
std::set<int>::iterator iter;
bool inserted;
std::tie(iter, inserted) = mySet.insert(10);
if (inserted)
{
  std::cout << "Value was inserted\n";
}

// since C++17
std::set<int> mySet;
auto [iter, inserted] = mySet.insert(10);

Structured Binding Declaration cannot be declared constexpr (might be solved in C++20 by the proposal P1481)

Binding

  1. If the initializer is an array
  2. If the initializer supports std::tuple_size<> and provides get<N>() and std::tuple_-element functions
class UserEntry {
public:
  void Load() { }
  std::string GetName() const { return name; }
  unsigned GetAge() const { return age; }
private:
  std::string name;
  unsigned age { 0 };
  size_t cacheEntry { 0 }; // not exposed
};

// The interface for Structured Bindings
// with if constexpr:
template <size_t I> auto get(const UserEntry& u) {
  if constexpr (I == 0) return u.GetName();
  else if constexpr (I == 1) return u.GetAge();
}
namespace std {
  template <> struct tuple_size<UserEntry> : std::integral_constant<size_t, 2> { };
  template <> struct tuple_element<0,UserEntry> { using type = std::string; };
  template <> struct tuple_element<1,UserEntry> { using type = unsigned; };
}

// Usage example:
UserEntry u;
u.Load();
auto [name, age] = u; // read access
std:: cout << name << ", " << age << '\n';
  1. If the initializer’s type contains only non static, public members. The class doesn’t have to be POD, but the number of identifiers must equal the number of non-static data members.
struct Point {
  double x;
  double y;
};
Point GetStartPoint() {
  return { 0.0, 0.0 };
}
const auto [x, y] = GetStartPoint();

Init Statement for if and switch

Inline Variables

// inside a header file:
struct MyClass
{
  static const int sValue;
};
// later in the same header file:
inline int const MyClass::sValue = 777;
// there’s no need to use constexpr
struct MyClass
{
  inline static const int sValue = 777;
};

constexpr Lambda Expressions

Nested Namespaces

4. Templates

Template Argument Deduction for Class Templates

Note, that partial deduction cannot happen, you have to specify all the template parameters or none:

std::tuple t(1, 2, 3); // OK: deduction
std::tuple<int,int,int> t(1, 2, 3); // OK: all arguments are provided
std::tuple<int> t(1, 2, 3); // Error: partial deduction
// control block and int might be in different places in memory
std::shared_ptr<int> p(new int{10});

// the control block and int are in the same contiguous memory section
auto p2 = std::make_shared<int>(10);

Deduction Guides

template<typename T>
struct MyType
{
  T str;
};
// custom deduction guide
MyType(const char *) -> MyType<std::string>;
MyType t{"Hello World"};
template<class... Ts>
struct overload : Ts... { using Ts::operator()...; };

template<class... Ts>
overload(Ts...) -> overload<Ts...>; // deduction guide

Fold Expressions

template<typename ...Args>
void FoldSeparateLine(Args&&... args)
{
  auto separateLine = [](const auto& v) {
    std::cout << v << '\n';
  };
  (... , separateLine (std::forward<Args>(args))); // over comma operator
}

if constexpr

Declaring Non-Type Template Parameters With auto

// In C++11/C++14
template <typename Type, Type value> constexpr Type TConstant = value;
constexpr auto const MySuperConst = TConstant<int, 100>;

// Since C++17
template <auto value> constexpr auto TConstant = value;
constexpr auto const MySuperConst = TConstant<100>;

template <auto ... vs> struct HeterogenousValueList {};
using MyList = HeterogenousValueList<'a', 100, 'b'>;

Other Changes

Allow typename in a template template parameters.

Allow constant evaluation for all non-type template arguments

Variable templates for traits

Pack expansions in using-declarations

template<class... Ts> struct overloaded : Ts... {
  using Ts::operator()...;
};

Logical operation metafunctions

  • template<class... B> struct conjunction; - logical AND
  • template<class... B> struct disjunction; - logical OR
  • template<class B> struct negation; - logical negation

5. Standard Attributes

C++11:

  • [[noreturn]]
  • [[carries_dependency]]: Indicates that the dependency chain in release-consume std::memory_order propagates in and out of the function, which allows the compiler to skip unnecessary memory fence instructions. Mostly to help to optimise multi-threaded code and when using different memory models.

C++14:

  • [[deprecated]] and [[deprecated("reason")]]

C++17:

  • [[fallthrough]]
  • [[maybe_unused]]
  • [[nodiscard]]
enum class [[nodiscard]] ErrorCode {
  OK,
  Fatal,
  System,
  FileIssue
};

ErrorCode OpenFile(std::string_view fileName);
ErrorCode SendEmail(std::string_view sendto, std::string_view text);
ErrorCode SystemCall(std::string_view text);

Attributes for namespaces and enumerators

Ignore unknown attributes

Using attribute namespaces without repetition

There is a reasonable fear that attributes will be used to create language dialects. The recommendation is to use attributes to only control things that do not affect the meaning of a program but might help detect errors (e.g. [[noreturn]]) or help optimizers (e.g. [[carries_dependency]]).

Part 2 - The Standard Library Changes

6. std::optional

When to Use

If you want to represent a nullable type.

Return a result of some computation (processing) that fails to produce a value and is not an error.

To perform lazy-loading of resources.

To pass optional parameters into functions.

std::optional Creation

  • Initialise as empty
  • Directly with a value
  • With a value using deduction guides
  • By using make_optional
  • With std::in_place
  • From other optional

So what’s the advantage of using std::in_place_t in std::optional? There are at least two important reasons:

  • Default constructor
  • Efficient construction for constructors with many arguments
auto opt = std::make_optional<UserName>();
auto opt = std::make_optional<Point>(0, 0);

// Is as efficient as:

std::optional<UserName> opt{std::in_place};
std::optional<Point> opt{std::in_place_t, 0, 0};

Be Careful With Braces when Returning

std::optional<std::string> CreateString()
{
  std::string str {"Hello Super Awesome Long String"};
  return {str}; // this one will cause a copy
  // return str; // this one moves
}

According to the Standard if you wrap a return value into braces {} then you prevent move operations from happening. The returned object will be copied only.

Accessing The Stored Value

  • operator* and operator-> - if there’s no value the behaviour is undefined!
  • value() - returns the value, or throws std::bad_optional_access
  • value_or(defaultVal) - returns the value if available, or defaultVal otherwise
// compute string function:
std::optional<std::string> maybe_create_hello();
// ...
if (auto ostr = maybe_create_hello(); ostr)
{
  std::cout << "ostr " << *ostr << '\n';
}
else
{
  std::cout << "ostr is null\n";
}

Examples of std::optional

Performance & Memory Consideration

template <typename T>
class optional
{
  bool _initialized;
  std::aligned_storage_t<sizeof(t), alignof(T)> _storage;
public:
  // operations
};

Special case: optional<bool> and optional<T*>

Summary

  • std::optional is a wrapper type to express “null-able” types
  • std::optional won’t use any dynamic allocation
  • std::optional contains a value or it’s empty – use operator *, operator->, value() or value_or() to access the underlying value.
  • std::optional is implicitly converted to bool so that you can easily check if it contains a value or not

7. std::variant

The Basics

C.183: Don’t use a union for type punning

Reason: It is undefined behaviour to read a union member with a different type from the one with which it was written. Such punning is invisible, or at least harder to spot than using a named cast. Type punning using a union is a source of errors.

When to Use

  • All the places where you might get a few types for a single field: so things like parsing command lines, ini files, language parsers, etc.
  • Expressing several possible outcomes of a computation efficiently: like finding roots of equations.
  • Error handling - for example you can return variant<Object, ErrorCode>. If the value is available, then you return Object otherwise you assign some error code.
  • Finite State Machines.
  • Polymorphism without vtables and inheritance (thanks to the visitor pattern).

A Functional Background

variant types (also called a tagged union, a discriminated union, or a sum type) come from the functional language world and Type Theory

std::variant Creation

when the first alternative doesn’t have a default constructor, you can place std::monostate as the first alternative (or you can also shuffle the types, and find the type with a default constructor).

// monostate for default initialisation:
class NotSimple
{
public:
  NotSimple(int, float) { }
};

// std::variant<NotSimple, int> cannotInit; // error
std::variant<std::monostate, NotSimple, int> okInit;
std::cout << okInit.index() << '\n';

Changing the Values

  • the assignment operator
  • emplace
  • get and then assign a new value for the currently active type
  • a visitor (you cannot change the type, but you can change the value of the current alternative)

Object Lifetime

std::variant<std::string, int> v { "Hello A Quite Long String" };
// v allocates some memory for the string
v = 10; // we call destructor for the string!
// no memory leak

Visitors for std::variant

template <class Visitor, class... Variants>
constexpr visit(Visitor&& vis, Variants&&... vars);
auto PrintVisitor = [](const auto& t) { std::cout << t << '\n'; };
auto TwiceMoreVisitor = [](auto& t) { t*= 2; };

std::variant<int, float> intFloat { 20.4f };
std::visit(PrintVisitor, intFloat);
std::visit(TwiceMoreVisitor, intFloat);
std::visit(PrintVisitor, intFloat);

struct MultiplyVisitor
{
  float mFactor;
  MultiplyVisitor(float factor) : mFactor(factor) { }
  void operator()(int& i) const {
    i *= static_cast<int>(mFactor);
  }
  void operator()(float& f) const {
    f *= mFactor;
  }
  void operator()(std::string& ) const {
    // nothing to do here...
  }
};
std::visit(MultiplyVisitor(0.5f), intFloat);
std::visit(PrintVisitor, intFloat);

Overload

template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
template<class... Ts> overload(Ts...) -> overload<Ts...>;

std::variant<int, float, std::string> myVariant;
std::visit(
  overload {
    [](const int& i) { std::cout << "int: " << i; },
    [](const std::string& s) { std::cout << "string: " << s; },
    [](const float& f) { std::cout << "float: " << f; }
  },
  myVariant
);

overload uses three C++17 features:

  • Pack expansions in using declarations - short and compact syntax with variadic templates.

  • Custom template argument deduction rules - this allows the compiler to deduce types of lambdas that are the base classes for the pattern. Without it we’d have to define a “make” function.

  • Extension to aggregate Initialisation - the overload pattern uses aggregate initialisation to init base classes. Before C++17 it was not possible.

  • 2 Lines Of Code and 3 C++17 Features - The overload Pattern

Performance & Memory Considerations

std::variant uses the memory in a similar way to union: so it will take the max size of the underlying types. But since we need something that will know what the currently active alternative is, then we need to use some more space.

What’s more interesting is that std::variant won’t allocate any extra space! No dynamic allocation happens to hold variants or the discriminator.

Wrap Up

  • It holds one of several alternatives in a type-safe way
  • No extra memory allocation is needed. The variant needs the size of the max of the sizes of the alternatives, plus some little extra space for knowing the currently active value
  • By default, it initialises with the default value of the first alternative
  • You can assess the value by using std::get, std::get_if`` or by using a form of a visitor
  • To check the currently active type you can use std::holds_alternative or std::variant::index
  • std::visit is a way to invoke an operation on the currently active type in the variant. It’s a callable object with overloads for all the possible types in the variant(s)
  • Rarely std::variant might get into invalid state, you can check it via valueless_by_-exception

8. std::any

The Basics

  • std::any is not a template class like std::optional or std::variant.
  • by default it contains no value, and you can check it via .has_value().
  • you can reset an any object via .reset().
  • it works on “decayed” types - so before assignment, initialisation, or emplacement the type is transformed by std::decay.
  • when a different type is assigned, then the active type is destroyed.
  • you can access the value by using std::any_cast<T>. It will throw bad_any_cast if the active type is not T.
  • you can discover the active type by using .type() that returns std::type_info of the type.

When to Use

While void* might be an extremely unsafe pattern with some limited use cases, std::any adds type-safety, and that’s why it has more applications.

std::any Creation

  • a default initialisation - then the object is empty
  • a direct initialisation with a value/object
  • in place std::in_place_type
  • via std::make_any

Object Lifetime

The crucial part of being safe for std::any is not to leak any resources. To achieve this behaviour std::any will destroy any active object before assigning a new value.

Accessing The Stored Value

template<class ValueType> ValueType std::any_cast:

  • read access - returns a copy of the value, and throws std::bad_any_cast when it fails
  • read/write access - returns a reference, and throws std::bad_any_cast when it fails
  • read/write access - returns a pointer to the value (const or not) or nullptr on failure

Performance & Memory Considerations

  • std::any requires extra dynamic memory allocations.
  • Implementations are encouraged to use SBO - Small Buffer Optimisation.

Wrap Up

  • std::any is not a template class
  • std::any uses Small Buffer Optimisation, so it will not dynamically allocate memory for simple types like ints, doubles… but for larger types, it will use extra new.
  • std::any might be considered ‘heavy’, but offers a lot of flexibility and type-safety.
  • you can access the currently stored value by using any_cast that offers a few “modes”: for example it might throw an exception or return nullptr.
  • use it when you don’t know the possible types - in other cases consider std::variant.

9. std::string_view

When to Use

  • Optimisation: you can carefully review your code and replace various string operations with string_view. In most cases, you should end up with faster code and fewer memory allocations.

  • As a possible replacement for const std::string& parameter - especially in functions that don’t need the ownership and don’t store the string.

  • Handling strings coming from other API: QString, CString, const char*... everything that is placed in a contiguous memory chunk and has a basic char-type. You can write a function that accepts string_view and no conversion from that other implementation will happen.

  • string_view is only a non-owning view, so if the original object is gone, the view becomes rubbish and you might get into trouble.

  • string_view might not contain null terminator

Risks Using string_view

Taking Care of Not Null-Terminated Strings

  • string_view is problematic with all functions that accept traditional C-strings because string_view breaks with C-string termination assumptions. If a function accepts only a const char* parameter, it’s probably a bad idea to pass string_view into it. On the other hand, it might be safe when such a function accepts const char* and length parameters.
  • Conversion into strings - you need to specify not only the pointer to the contiguous character sequence but also the length.

References and Temporary Objects

Reference Lifetime Extension

the lifetime of a temporary object bound to a const reference is prolonged to the lifetime of the reference itself.

std::string func()
{
  std::string s;
  // build s...
  return s;
}
std::string_view sv = func();
// no temp lifetime extension!

Handling Non-Null Terminated Strings

std::string s = "Hello World";
std::string_view sv = s;
std::string_view sv2 = sv.substr(0, 5);
printf("My String %s", sv2.data()); // oops?
printf("%.*s\n", static_cast<int>(sv2.size()), sv2.data()); // fix
std::string number = "123.456";
std::string_view svNum { number.data(), 3 };
auto f = atof(svNum.data()); // should be 123, but is 123.456!
std::cout << f << '\n';

// Fix: use from_chars (C++17)
int res = 0;
std::from_chars(svNum.data(), svNum.data()+svNum.size(), res);
std::cout << res << '\n';

// Fix (General solution)
std::string tempStr { svNum.data(), svNum.size() };
auto f = atof(tempStr.c_str());
std::cout << f << '\n';

Performance & Memory Considerations

  • string_view is usually implemented as [ptr, len] - one pointer and usually size_t to represent the possible size.

If we consider the std::string type, due to common Small String Optimisations std::string is usually 24 or 32 bytes, so double the size of string_view. If a string is longer than the SSO buffer then std::string allocates memory on the heap. If SSO is not supported (which is rare), then std::string would consist of a pointer to the allocated memory and the size.

  • substr is just a copy of two elements in string_view, while string will perform a copy of a memory range. The complexity is O(1) vs O(n).

Examples

String Split

std::vector<std::string_view>
splitSV(std::string_view strv, std::string_view delims = " ")
{
  std::vector<std::string_view> output;
  auto first = strv.begin();
  while (first != strv.end())
  {
    const auto second = std::find_first_of(first, std::cend(strv), std::cbegin(delims), std::cend(delims));
    if (first != second)
    {
      output.emplace_back(strv.substr(std::distance(strv.begin(), first), std::distance(first, second)));
    }
    if (second == strv.end()) break;
    first = std::next(second);
  }
  return output;
}

// Usage example
const std::string str {
  "Hello Extra,,, Super, Amazing World"
};
for (const auto& word : splitSV(str, " ,"))
{
  std::cout << word << '\n';
}

Wrap Up

  • It’s a specialisation of std::basic_string_view<charType, traits<charType>> - with charType equal to char.
  • It’s a non-owning view of a contiguous sequence of characters.
  • It might not include null terminator at the end.
  • It can be used to optimise code and limit the need for temporary copies of strings.
  • It contains most of std::string operations that don’t change the underlying characters.
  • Its operations are also marked as constexpr. But:
  • Make sure the underlying sequence of characters is still present!
  • While std::string_view looks like a constant reference to the string, the language doesn’t extend the lifetime of returned temporary objects that are bound to std::string_view.
  • Always remember to use stringView.size() when you build a string from string_view. The size() method properly marks the end of string_view.
  • Be careful when you pass string_view into functions that accept null-terminated strings unless you’re sure your string_view contains a null terminator.

10. String Conversions

11. Searchers & String Matching

Overview of String Matching Algorithms

12. Filesystem

13. Parallel STL Algorithms

Limitations and Unsafe Instructions

  • When using par execution policy try to access the shared resources as little as possible.
  • Don’t use synchronisation and memory allocation when executing with par_unseq policy.

Chapter Summary

  • Parallel STL gives you set of 69 algorithms that have overloads for the execution policy parameter.
  • Execution policy describes how the algorithm might be executed.
  • There are three execution policies in C++17 (<execution> header) – std::execution::seq - sequential – std::execution::par - parallel – std::execution::par_unseq - parallel and vectorised
  • In parallel execution policy functors that are passed to algorithms cannot cause deadlocks and data races
  • In parallel unsequenced policy functors cannot call vectorised unsafe instructions like memory allocations or any synchronisation mechanisms
  • To handle new execution patterns there are also new algorithms: like std::reduce, excelusive_scan - They work out of order so the operations must be associative to generate deterministic results
  • There are “fused” algorithms: transform_reduce, transform_exclusive_scan, transform_inclusive_scan that combine two algorithms together.
  • Assuming there are no synchronisation points in the parallel execution, the parallel algorithms should be faster than the sequential version. Still, they perform more work - especially the setup and divisions into tasks.
  • Implementations might usually use some thread pools to implement a parallel algorithm on CPU.

14. Other Changes In The Library

std::byte

Improvements for Maps and Sets

  • try_emplace() - if the object already exists then it does nothing, otherwise it behaves like emplace(). – emplace() might move from the input parameter when the key is in the map, that’s why it’s best to use find() before such emplacement.
  • insert_or_assign() - gives more information than operator[] - as it returns if the element was newly inserted or updated and also works with types that have no default constructor.

Return Type of Emplace Methods

// since C++11 and until C++17 for std::vector
template< class... Args >
void emplace_back( Args&&... args );

// since C++17 for std::vector
template< class... Args >
reference emplace_back( Args&&... args );

Sampling Algorithms

New Mathematical Functions

  • std::gcd (Greatest Common Divisor)
  • std::lcm (Least Common Multiple)
  • std::clamp

Shared Pointers and Arrays

Non-member size(), data() and empty()

constexpr Additions to the Standard Library

std::scoped_lock

std::iterator Is Deprecated

Polymorphic Allocator, pmr

  • std::pmr::memory_resource - is an abstract base class for all other implementations. It defines the following pure virtual methods: do_allocate, do_deallocate and do_is_equal.

  • std::pmr::polymorphic allocator - is an implementation of a standard allocator that uses memory_resource object to perform memory allocations and deallocations.

  • global memory resources accessed by new_delete_resource() and null_memory_resource()

  • a set of predefined memory pool resource classes: synchronized_pool_resource, unsynchronized_pool_resource, and monotonic_buffer_resource

  • template specialisations of the standard containers with polymorphic allocator, for example std::pmr::vector, std::pmr::string, std::pmr::map and others. Each specialisation is defined in the same header file as the corresponding container.

  • Allocators: the Good Parts

⚠️ **GitHub.com Fallback** ⚠️