Skip to content

Latest commit

 

History

History
2050 lines (1604 loc) · 31.3 KB

ALL_IN_ONE.md

File metadata and controls

2050 lines (1604 loc) · 31.3 KB

Descriptions of C++17 features, presented mostly in "Tony Tables" (hey, the tables were my idea, but their name wasn't :-).

There are actually over 100 changes in C++17, only some of them are listed here.

Caveat1: At time of writing, C++17 was completed, but not signed off yet. There may be slight differences, although highly unlikely (modulo defects).
Caveat2: I make mistakes. This is more likely :-)

Created with the support of my employer, Christie Digital Systems.


if-init

C++
{
   if (Foo * ptr = get_foo())
      use(*ptr);
   more_code();
}

But what do you do when it isn't (convertible to) boolean?

C++
{
   {
      QVariant var = getAnswer();
      if (var.isValid())
         use(var);
   }
   more_code();
}
C++ C++17
{
   {
      QVariant var = getAnswer();
      if (var.isValid())
         use(var);
   }
   more_code();
}
{
   
   
   if (QVariant var = getAnswer(); var.isValid())
      use(var);
      
   more_code();
}

Switch statements too!

C++17
{
   switch (Device dev = get_device(); dev.state())
   {
   case sleep: /*...*/ break;
   case ready: /*...*/ break;
   case bad: /*...*/ break;
   }
}

Structured Bindings

C++14 C++17
   tuple<int, string> func();
   
   auto tup = func();
   int i = get<0>(tup);
   string s = get<1>(tup);
  
   use(s, ++i);
   tuple<int, string> func();
   
   int i;
   string s;
   std::tie(i,s) = func();

   use(s, ++i);
   tuple<int, string> func();
   
   
   auto [ i, s ] = func();


   use(s, ++i);
C++17 compiler
   pair<int, string> func();
   
   
   auto [ i, s ] = func();


   use(s, ++i);
   pair<int, string> func();
   
   auto __tmp = func();
   auto & i = get<0>(__tmp);
   auto & s = get<1>(__tmp);

   use(s, ++i);

Note, in the above, __tmp is a copy, but i and s are references. Or I should say "references" in quotes. Not exactly references, but real compiler synonyms for the members. (They are not real references as things like decltype "look through" the references to the actual members.)

So even though auto [i,s] = func(); has no & anywhere, there are still references involved. For example:

C++17 compiler
#include <string>
#include <iostream>

struct Foo
{
   int x = 0;
   std::string str = "world";
   ~Foo() { std::cout << str; }
};

int main()
{
    auto [ i, s ] = Foo();
    std::cout << "hello ";
    s = "structured bindings";
}
#include <string>
#include <iostream>

struct Foo
{
   int x = 0;
   std::string str = "world";
   ~Foo() { std::cout << str; }
};

int main()
{
    auto __tmp = Foo();
    std::cout << "hello ";
    __tmp.str = "structured bindings";
}
Output
hello structured bindings

Note that the s = "structured bindings"; is modifying Foo::str inside of the temporary (hidden) Foo, so that when the temporary Foo is destroyed, its destructor prints structured bindings instead of world.

So what does a & do in a structured binding declaration?
It gets applied to the hidden __tmp variable:

C++17 compiler
   struct X { int i = 0; };
   X makeX();
   
   X x;
   
   auto [ b ] = makeX();
   b++;
   auto const [ c ] = makeX();
   c++;
   auto & [ d ] = makeX();
   d++;
   auto & [ e ] = x;
   e++;
   auto const & [ f ] = makeX();
   f++;
   struct X { int i = 0; };
   X makeX();
   
   X x;
   
   auto __tmp1 = makeX();
   __tmp1.i++;
   auto const __tmp2 = makeX();
   __tmp2.i++; //error: can't modify const
   auto & __tmp3 = makeX(); //error: non-const ref cannot bind to temp
   
   auto & _tmp3 = x;
   x.i++;
   auto const & _tmp4 = makeX();
   __tmp4.i++; //error: can't modify const

Wait, pair and tuple are not magic (just nearly impossible to write to STL quality), can my types work with this?

YES. The compiler uses get<N>() if available, or can work with plain structs directly:

Structs

C++17 compiler
   struct Foo {
      int x;
      string str;
   };
   
   Foo func();
     
     
   auto [ i, s ] = func();


   use(s, ++i);
   struct Foo {
      int x;
      string str;
   };
   
   Foo func();
   
   Foo __tmp = func();
   auto & i = __tmp.x;
   auto & s = __tmp.str;

   use(s, ++i);

Implement your own get(), tuple_size, tuple_element

For any class/struct that doesn't work by default, you need to implement your own custom get<>() and you also need to implement tuple_size and tuple_element.

C++17
   class Foo {
      // ...
   public:
      template <int N> auto & get() /*const?*/ { /*...*/ }
   };
   // or get outside class
   template<int N> auto & get(Foo /*const?*/ & foo) { /*...*/ }
   //...
   
   // tuple_size/element specialized
   // yes, in namespace std
   namespace std {
      // how many elements does Foo have
      template<> struct tuple_size<Foo> { static const int value = 3; }
      // what type is element N
      template<int N> struct tuple_element<N, Foo> { using type = ...add code here...; }
   }
   
   Foo func();

   auto [ i, s ] = func();

   use(s, ++i);

Arrays, std::array, etc, oh my!

etc
    
   int arr[4] = { /*...*/ };
   auto [ a, b, c, d ] = arr; 
   auto [ t, u, v ] = std::array<int,3>();
   
   // now we're talkin'
   for (auto && [key, value] : my_map)
   {
      //...
   }

Constexpr If

Actually, how would someone write a custom get<>() function for their class? (see Structured Bindings for why you might want to do that) Since each get<0>, get<1>, etc returns a different member, which are possibly different types... (oh no, template metaprogramming...)

C++14 C++17
class Foo {
  int myInt;
  string myString;
public:
  int const & refInt() const
  { return myInt; }
  string const & refString() const
  { return myString; }
};

namespace std
{
   template<> class tuple_size<Foo>
       : public integral_constant<int, 2>
   { };
   template<int N> class tuple_element<N, Foo>
   {
   public:
      using type =
      conditional_t<N==0,int const &,string const &>;
   };
}

template<int N> std::tuple_element_t<N,Foo>
get(Foo const &);

// here's some specializations (the real stuff)
template<> std::tuple_element_t<0,Foo>
get<0>(Foo const & foo)
{
  return foo.refInt();
}
template<> std::tuple_element_t<1,Foo>
get<1>(Foo const & foo)
{
  return foo.refString();
}
class Foo {
  int myInt;
  string myString;
public:
  int const & refInt() const
  { return myInt; }
  string const & refString() const
  { return myString; }
};

namespace std
{
   template<> class tuple_size<Foo>
       : public integral_constant<int, 2>
   { };
   template<int N> class tuple_element<N, Foo>
   {
   public:
      using type =
      conditional_t<N==0,int const &,string const &>;
   };
}


template<int N> auto & get(Foo const & foo)
{
  static_assert(0 <= N && N < 2, "Foo only has 2 members");

  if constexpr (N == 0)  // !! LOOK HERE !!
     return foo.refInt();
  else if constexpr (N == 1)    // !! LOOK HERE !!
     return foo.refString();
}

P.S. if constexpr (expression) doesn't check if the expression is constexpr. The expression must be constexpr (else it doesn't compile). The part that is constexpr is 'doing' the if. Don't think about this and what syntax might be better. The committee argued about it long enough.

Deduction Guides

Speaking of pair and tuple...

C++14 C++17
pair<int, string> is1 = pair<int, string>(17, "hello");
auto is2 = std::pair<int, string>(17, "hello");
auto is3 = std::make_pair(17, string("hello"));
auto is4 = std::make_pair(17, "hello"s);
pair<int, string> is1 = pair(17, "hello");
auto is2 = pair(17, "hello"); // !! pair<int, char const *>
auto is3 = pair(17, string("hello"));
auto is4 = pair(17, "hello"s);

The magic behind the above is called "deduction guides". In particular, implicit deduction guides, and explicit deduction guides.

Explicit Deduction Guides

template<typename T>
struct Thingy
{
  T t;
};

// !! LOOK HERE !!
Thingy(const char *) -> Thingy<std::string>;

Thingy thing{"A String"}; // thing.t is a `std::string`.

(example from "Nicol Bolas")

Implicit Deduction Guides

For any template<typename T, typename U, etc> struct... (or class!) if there is a constructor that takes T and U such that it can figure out all the types, then that constructor forms an "implicit" deduction guide. ie just like the explicit one above, but the compiler does it for you.

More importantly, the above should say for all templatized types... ie whether you want it or not.

template<auto>

C++14 C++17
template <typename T, T v>
struct integral_constant
{
   static constexpr T value = v;
};
integral_constant<int, 2048>::value
integral_constant<char, 'a'>::value
template <auto v>
struct integral_constant
{
   static constexpr auto value = v;
};
integral_constant<2048>::value
integral_constant<'a'>::value

Fold Expressions

How do you write `sum()` ?
auto x = sum(5, 8);
auto y = sum(a, b, 17, 3.14, etc);
C++14 C++17
auto sum() { return 0; }

template <typename T>
auto sum(T&& t) { return t; }

template <typename T, typename... Rest>
auto sum(T&& t, Rest&&... r) {
   return t + sum(std::forward<Rest>(r)...);
}



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

Nested Namespaces

C++14 C++17
   namespace A {
      namespace B {
         namespace C {
            struct Foo { };
            //...
         }
      }
   }
   namespace A::B::C {
      struct Foo { };
      //...
   }

Single Param static_assert

C++14 C++17
static_assert(sizeof(short) == 2, "sizeof(short) == 2")
static_assert(sizeof(short) == 2)
Output
static assertion failure: sizeof(short) == 2

Inline Variables

C++14 C++17
// foo.h
extern int foo;

// foo.cpp
int foo = 10;
// foo.h
inline int foo = 10;
C++14 C++17
// foo.h
struct Foo {
   static int foo;
};

// foo.cpp
int Foo::foo = 10;
// foo.h
struct Foo {
   static inline int foo = 10;
};

Guaranteed Copy Elision

C++17
// header <mutex>
namespace std
{
   template <typename M>
   struct lock_guard
   {
      explicit lock_guard(M & mutex);
      // not copyable, not movable:
      lock_guard(lock_guard const & ) = delete;
      //...
   }
}

// your code
lock_guard<mutex> grab_lock(mutex & mtx)
{
   return lock_guard<mutex>(mtx);
}

mutex mtx;

void foo()
{
   auto guard = grab_lock(mtx);
   /* do stuff holding lock */
}

some new [[attributes]]

[[fallthrough]]

C++14 C++17
switch (device.status())
{
case sleep:
   device.wake();
   // fall thru
case ready:
   device.run();
   break;
case bad:
   handle_error();
   break;
}
switch (device.status())
{
case sleep:
   device.wake();
   [[fallthrough]];
case ready:
   device.run();
   break;
case bad:
   handle_error();
   break;
}
Compiler Compiler
warning: case statement without break

[[nodiscard]]

On functions:

C++14 C++17
struct SomeInts
{
   bool empty();
   void push_back(int);
   //etc
};

void random_fill(SomeInts & container,
      int min, int max, int count)
{
   container.empty(); // empty it first
   for (int num : gen_rand(min, max, count))
      container.push_back(num);
}
struct SomeInts
{
   [[nodiscard]] bool empty();
   void push_back(int);
   //etc
};

void random_fill(SomeInts & container,
      int min, int max, int count)
{
   container.empty(); // empty it first
   for (int num : gen_rand(min, max, count))
      container.push_back(num);
}
Compiler C++17 Compiler
warning: ignoring return value of 'bool empty()'

On classes or structs:

C++14 C++17
struct MyError {
  std::string message;
  int code;
};

MyError divide(int a, int b) {
  if (b == 0) {
    return {"Division by zero", -1};
  }

  std::cout << (a / b) << '\n';

  return {};
}

divide(1, 2);
struct [[nodiscard]] MyError {
  std::string message;
  int code;
};

MyError divide(int a, int b) {
  if (b == 0) {
    return {"Division by zero", -1};
  }

  std::cout << (a / b) << '\n';

  return {};
}

divide(1, 2);
Compiler C++17 Compiler
warning: ignoring return value of function declared with 'nodiscard' attribute

Advice: use [[nodiscard]] sparingly. ie only when there really is no reason to ignore the value.

[[maybe_unused]]

C++14 C++17
   bool res = step1();
   assert(res);
   step2();
   etc();
   [[maybe_unused]] bool res = step1();
   assert(res);
   step2();
   etc();
Compiler C++17 Compiler
warning: unused variable 'res'
C++17
   [[maybe_unused]] void f()
   {
      /*...*/
   }
   int main()
   {
   }

std::string_view

Standardization of existing practice. See boost::string_ref, QStringRef, etc.


Let's say I write some kind of parser:

Foo parseFoo(std::string const & input);

But then I have some users using char * - and creating a string just to pass to the parser, so I add (or change to) this interface:

Foo parseFoo(char const * str);

But this parser becomes really popular. Some are embedding Foos into the middle of their own formats - so no null at the end:

Foo parseFoo(char const * str, int length);

Oh, and we use a custom string class (or QString,...)

Foo parseFoo(MyString const & str);

etc! How do you maintain this interface?

C++14 C++17
Foo parseFoo(std::string const & input);
Foo parseFoo(char const * str);

Foo parseFoo(char const * str, int length);




Foo parseFoo(MyString const & str);
Foo parseFoo(std::string_view input);

// I would say don't offer this interface, but:
Foo parseFoo(char const * str, int length)
{
   return parseFoo(string_view(str,length));
}

class MyString {
   //...
   operator string_view() const
   {
      return string_view(this->data, this->length);
   }
};

Example 2

Think of something like an XML parser, that is constantly returning string objects for the XML entities that it finds. Each of those strings is a potential allocation. So instead, return string_view.

Caveats

string_view does NOT own the string memory. It points to memory owned elsewhere, similar to how a reference or pointer or iterator works. It has reference semantics.

std::optional<T>

So, we have

Foo parseFoo(std::string_view input);

What if the parse fails? And you can't parse out a Foo?

  1. throw an exception
  2. return default Foo. ie Foo() (if Foo is default constructible)
  3. bool parseFoo(std::string_view input, Foo & output); // also basically requires Foo()
  4. Foo * parseFoo(std::string_view input); // allocation!? :-(
C++14 C++17
// returns default Foo on error
Foo parseFoo(std::string_view in);

// throws parse_error
Foo parseFoo(std::string_view in);

// returns false on error
bool parseFoo(std::string_view in, Foo & output);

// returns null on error
unique_ptr<Foo> parseFoo(std::string_view in);



std::optional<Foo> parseFoo(std::string_view in);

Usage

C++17
optional ofoo = parseFoo(str);
if (ofoo)
   use(*ofoo);
// nicer with new if syntax:
if (optional ofoo = parseFoo(str); ofoo)
   use(*ofoo);
optional<int> oi = parseInt(str);
std::cout << oi.value_or(0);

Note, optional is not just for errors, and exceptions are still the go-to choice for error handling.
See also boost::optional, Haskell's Maybe, etc.

std::variant<A,B,C,...>

A more perfect union.

C++14 C++17
struct Stuff
{
   union Data {
      int i;
      double d;
      string s;  // constructor/destructor???
   } data;
   enum Type { INT, DOUBLE, STRING } type;
};
struct Stuff
{




   std::variant<int, double, string> data;

};

Usage

C++14 C++17
void handleData(int i);
void handleData(double d);
void handleData(string const & s);

//...

switch (stuff.type)
{
case INT:
   handleData(stuff.data.i);
   break;
case DOUBLE:
   handleData(stuff.data.d);
   break;
case STRING:
   handleData(stuff.data.s);
   break;
}
void handleData(int i);
void handleData(double d);
void handleData(string const & s);

//...

std::visit([](auto const & val) { handleData(val); }, stuff.data);

// can also switch(stuff.data.index())
How the above lambda works
struct ThatLambda
{
   void operator()(int const & i) { handleData(i); }
   void operator()(double const & d) { handleData(d); }
   void operator()(string const & s) { handleData(s); }
};

ThatLambda thatLambda;
std::visit(thatLambda, stuff.data);

More Usage

C++17
if (holds_alternative<int>(data))
   int i = get<int>(data);

// throws if not double:
double d = get<double>(data);
C++17
std::variant<Foo, Bar> var;  // calls Foo()
// (or doesn't compile if no Foo())

Bar bar = makeBar();
var = bar; // calls ~Foo() and Bar(Bar const &)
// (what if Bar(Bar const & b) throws?)

var = Foo(); // calls ~Bar() and move-ctor Foo(Foo &&)
// (what if Foo(Foo && b) throws? - even though moves shouldn't throw)

var = someFoo;  // calls Foo::operator=(Foo const &)


std::variant<Foo, std::string> foostr;

foostr = "hello"; // char * isn't Foo or string
// yet foostr holds a std::string

std::any

Whereas std::variant<A,B,C> can hold an A or B or C, std::any can hold (almost) anything!

C++14 C++17 C++17
void * v = ...;
if (v != nullptr) {
   // hope and pray it is an int:
   int i = *reinterpret_cast<int*>(v);
}
std::any v = ...;
if (v.has_value()) {
   // throws if not int
   int i = any_cast<int>(v);
}
std::any v = ...;
if (v.type() == typeid(int)) {
   // definitely an int
   int i = any_cast<int>(v);
}

Note: std::any is NOT a template. It can hold any types, and change type at runtime.

C++14 C++17
// can hold Circles, Squares, Triangles,...
std::vector<Shape *> shapes;
// can hold Circles, Squares, Triangles, ints, strings,...
std::vector<any> things;

namespace std::filesystem

C++14 Windows C++17
#include <windows.h>

void copy_foobar() {
  std::wstring dir = L"\\sandbox";
  std::wstring p = dir + L"\\foobar.txt";
  std::wstring copy = p;
  copy += ".bak";
  CopyFile(p, copy, false);
  
  std::string dir_copy = dir + ".bak";
  SHFILEOPSTRUCT s = { 0 };
  s.hwnd = someHwndFromSomewhere;
  s.wFunc = FO_COPY;
  s.fFlags = FOF_SILENT;
  s.pFrom = dir.c_str();
  s.pTo = dir_copy.c_str();
  SHFileOperation(&s);
}

void display_contents(std::wstring const & p) {
  std::cout << p << "\n";

  std::wstring search = p + "\\*";
  WIN32_FIND_DATA ffd;
  HANDLE hFind =
        FindFirstFile(search.c_str(), &ffd);
  if (hFind == INVALID_HANDLE_VALUE)
     return;
  
  do {
    if ( ffd.dwFileAttributes
         & FILE_ATTRIBUTE_DIRECTORY) {
      std::cout << "  " << ffd.cFileName << "\n";
    } else {
      LARGE_INTEGER filesize;
      filesize.LowPart = ffd.nFileSizeLow;
      filesize.HighPart = ffd.nFileSizeHigh;
      std::cout << "  " << ffd.cFileName
                << " [" << filesize.QuadPart
                << " bytes]\n";
    }
  } while (FindNextFile(hFind, &ffd) != 0);
}
#include <filesystem>
#include <iostream>
namespace fs = std::filesystem;

void copy_foobar() {
  fs::path dir = "/";
  dir /= "sandbox";
  fs::path p = dir / "foobar.txt";
  fs::path copy = p;
  copy += ".bak";
  fs::copy(p, copy);
  fs::path dir_copy = dir;
  dir_copy += ".bak";
  fs::copy(dir, dir_copy, fs::copy_options::recursive);
}

void display_contents(fs::path const & p) {
  std::cout << p.filename() << "\n";

  if (!fs::is_directory(p))
    return;

  for (auto const & e: fs::directory_iterator{p}) {
    if (fs::is_regular_file(e.status())) {
      std::cout << "  " << e.path().filename()
                << " [" << fs::file_size(e) << " bytes]\n";
    } else if (fs::is_directory(e.status())) {
      std::cout << "  " << e.path().filename() << "\n";
    }
  }
}
C++14 POSIX
#include <dirent.h>
#include <sys/stat.h>
#include <sys/types.h>

void copy_foobar() {

// [TODO]
// to copy file, use fread / fwrite

// how to copy directory...?
}

void display_contents(std::string const & p) {
  std::cout << p << "\n";

  struct dirent *dp;
  DIR *dfd;
  
  if ((dfd = opendir(p.c_str()) == nullptr)
    return;

  while((dp = readdir(dfd)) != nullptr) {
    struct stat st;
    string filename = p + "/" + dp->d_Name;
    if (stat(filename.c_str(), &st) == -1)
      continue;
      
    if ((st.st_mode & S_IFMT) == S_IFDIR)
      std::cout << "  " << filename << "\n";
    } else {
      std::cout << "  " << filename
                << " [" << st.st_size
                << " bytes]\n";
    }
  }
}

Parallel STL

A bunch of std:: algorithms can now run in parallel, if you request it.

adjacent_difference is_heap_until replace_copy_if
adjacent_find is_partitioned replace_if
all_of is_sorted reverse
any_of is_sorted_until reverse_copy
copy lexicographical_compare rotate
copy_if max_element rotate_copy
copy_n merge search
count min_element search_n
count_if minmax_element set_difference
equal mismatch set_intersection
fill move set_symmetric_difference
fill_n none_of set_union
find nth_element sort
find_end partial_sort stable_partition
find_first_of partial_sort_copy stable_sort
find_if partition swap_ranges
find_if_not partition_copy transform
generate remove uninitialized_copy
generate_n remove_copy uninitialized_copy_n
includes remove_copy_if uninitialized_fill
inner_product remove_if uninitialized_fill_n
inplace_merge replace unique
is_heap replace_copy unique_copy

How do you 'request' it?

C++14 C++17
std::for_each(first, last,
    [](auto & x){ process(x); }
);
std::copy(first, last, output);
std::sort(first, last);
std::transform(xfirst, xlast, yfirst,
    [=](double xi, double yi){ return a * xi + yi; }
);
std::for_each(std::execution::par, first, last,
    [](auto & x){ process(x); }
);
std::copy(std::execution::par, first, last, output);
std::sort(std::execution::par, first, last);
std::transform(std::execution::par_unseq, xfirst, xlast, yfirst,
    [=](double xi, double yi){ return a * xi + yi; }
);

Execution Policies

std::execution::seq indeterminately sequenced in the calling thread
std::execution::par multiple threads - calls are indeterminately sequenced with respect to each other within the same thread
std::execution::par_unseq multiple threads and may be vectorized - calls are unsequenced with respect to each other and possibly interleaved

Technical Specifications (TS)

Remember std::tr1 and std::tr2, which had shared_ptr etc (lots of Boost, basically), and later became C++11? Those were Technical Reports (thus the 'tr'). Now we call them Technical Specifications. (The differences are... technical. Basically, they are still specs, just not the Standard. They are specs, not "reports" about something, like a report on C++ performance wrt exceptions, etc (which is TR18015)

Also, we put them in std::experimental. It would be std::ish but I wasn't there that day :-(

The committee has a number of TSes on the go. Although they are not part of C++17, you can use them NOW.

Concepts TS

The biggest addition to C++ since sliced bread.

Available NOW in latest gcc.

In a nutshell,

C++14 Concepts TS
//
// T must be a Random Access Iterator
// otherwise you will either get the wrong answer,
// or, most likely, terrible compiler errors
//
template <typename T>
auto binarySearch(T first, T second)
{
   //...
}



template <RandomAccessIterator T>
auto binarySearch(T first, T second)
{
   //...
}
error: syntax error '[' unexpected
error: gibberish
error: more compiler gibberish
error: for pages and pages
...
error: MyIter does not model RandomAccessIterator

Modules TS

Encapsulation at the component level
(Precompiled headers on steroids. Don't tell Gaby I said that.)

Available NOW in Visual Studio and clang.

Coroutines TS

(a.k.a. Gor-routines.) Similar to await et al from C#, python, etc. But, of course, better.

Available NOW in Visual Studio.

Ranges TS

Nothing less than STL 2.0
https://github.com/ericniebler/range-v3

Networking TS

Boost ASIO.

Transactional Memory TS

Parallelism 2 TS

Concurrency 2 TS

Library Fundamentals 2 TS

2D Graphics TS


Some sources I used, and/or places for good C++17 info:

https://jfbastien.github.io/what-is-cpp17/#/
https://skebanga.github.io/structured-bindings/
https://www.bfilipek.com/2017/01/cpp17features.html
https://en.cppreference.com
https://stackoverflow.com
Bryce Adelstein Lelbach's talk, coming soon to C++Now 2017