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.
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;
}
} |
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)
{
//...
} |
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.
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.
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")
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.
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 |
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);
}
|
C++14 | C++17 |
---|---|
namespace A {
namespace B {
namespace C {
struct Foo { };
//...
}
}
} |
namespace A::B::C {
struct Foo { };
//...
} |
C++14 | C++17 |
---|---|
static_assert(sizeof(short) == 2, "sizeof(short) == 2") |
static_assert(sizeof(short) == 2) |
Output | |
static assertion failure: sizeof(short) == 2 |
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;
}; |
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 */
}
|
[[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()
{
} |
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.
So, we have
Foo parseFoo(std::string_view input);
What if the parse fails? And you can't parse out a Foo?
- throw an exception
- return default Foo. ie
Foo()
(if Foo is default constructible) bool parseFoo(std::string_view input, Foo & output);
// also basically requiresFoo()
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.
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 |
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; |
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";
}
}
} |
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; }
); |
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 |
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.
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 |
Encapsulation at the component level
(Precompiled headers on steroids. Don't tell Gaby I said that.)
Available NOW in Visual Studio and clang.
(a.k.a. Gor-routines.) Similar to await
et al from C#, python, etc. But, of course, better.
Available NOW in Visual Studio.
Nothing less than STL 2.0
https://github.com/ericniebler/range-v3
Boost ASIO.
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