C++17 is a major release, with over 100 new features or significant changes. In terms of big new features, there's nothing as significant as the rvalue references we saw in C++11, but there are a lot of improvements and additions, such as structured bindings and new container types. What’s more, a lot has been done to make C++ more consistent and remove unhelpful and unnecessary behavior, such as support for trigraphs and std::auto_ptr
.
This article discusses two significant C++17 upgrades that developers need to adopt when writing their own C++ code. I’ll explore structured bindings, which provide a useful new way to work with structured types, and then some of the new types and containers that have been added to the Standard Library.
Structured bindings are completely new and very useful. They let you perform multiple assignments from structured types such as tuples, arrays and structs, such as assigning all the members of a struct to individual variables in a single assignment statement. This can make code more compact and easier to understand.
The example code for structured bindings has been run on Linux using clang++ version 4 with the -std=c++1z
flag to enable C++17 features.
C++11 introduced tuples, which are analogous to arrays in that they are fixed-length collections, but which can contain a mixture of types. You can use a tuple to return more than one value from a function, like this:
#include
auto
get
()
{
return
std
::
make_tuple
(
"fred"
,
42
);
}
This simple code returns a tuple containing two items, and since C++14, you can use auto
with function return types, which makes the declaration of this function much tidier than it would otherwise be. Calling the function is simple, but getting the values out of the tuple can be rather ugly and unintuitive, requiring you to use std::get
:
auto
t
=
get
();
std
::
cout
<<
std
::
get
<
0
>
(
t
)
<<
std
::
endl
;
You can also use std::tie
to bind tuple members to variables, which you need to declare first:
std
::
string
name
;
int
age
;
std
::
tie
(
name
,
age
)
=
get
();
Using structured bindings in C++17, however, you can bind the tuple members directly to named variables without having to use std::get
, or declaring the variables first:
auto
[
name
,
age
]
=
get
();
std
::
cout
<<
name
<<
" is "
<<
age
<<
std
::
endl
;
This technique will also allow you to obtain references to tuple members, something that isn't possible using std::tie
. Here, you’ll get references to the tuple members, and when you change the value of one of them, the value in the tuple changes:
auto
t2
=
std
::
make_tuple
(
10
,
20
);
auto
&
[
first
,
second
]
=
t2
;
first
+=
1
;
std
::
cout
<<
"value is now "
<<
std
::
get
<
0
>
(
t2
)
<<
std
::
endl
;
The output will show that first member of t2
has changed from 10 to 11.
Tuples provide an obvious use case, but structured bindings can also be used with arrays and structs, for example:
struct
Person
{
std
::
string
name
;
uint32_t
age
;
std
::
string
city
;
};
Person
p1
{
"bill"
,
60
,
"New York"
};
auto
[
name
,
age
,
city
]
=
p1
;
std
::
cout
<<
name
<<
"("
<<
age
<<
") lives in "
<<
city
<<
std
::
endl
;
Arrays work in the same way:
std
::
array
<
int32_t
,
6
>
arr
{
10
,
11
,
12
,
13
,
14
,
15
};
auto
[
i
,
j
,
k
,
l
,
_dummy1
,
_dummy2
]
=
arr
;
There are a couple of drawbacks to this implementation.
The first limitation—which is also a limitation for std::tie
—is that you have to bind to all the elements, so that you can't, for example, retrieve just the first four elements of the array. If you want to retrieve parts of the structure or array, just provide dummy variables for the members you don't want, as illustrated in the array example.
The second limitation, which will be a disappointment to anyone who has used this same idea in functional languages such as Scala and Clojure, is that the destructuring works only one level deep. For example, suppose my Person
struct has a Location
member:
struct
Location
{
std
::
string
city
;
std
::
string
country
;
};
struct
Person
{
std
::
string
name
;
uint32_t
age
;
Location
loc
;
};
I can construct a Person
and Location
using nested initialization:
Person2
p2
{
"mike"
,
50
,
{
"Newcastle"
,
"UK"
}};
You might suppose that I could use binding to access the members like this, but you'll find it isn't valid:
auto
[
n
,
a
,
[
c1
,
c2
]]
=
p2
;
// won't compile
As a final note, this form of retrieving members works only for classes where the data you want is public and non-static. You can find more details in a blog post about structured binding.
In C++17, the Standard Library has also added many useful new data types, several of which originated in Boost.
The code in this section has been run in Visual Studio 2017.
Perhaps the simplest type is std::byte
, which represents a single byte. Developers have traditionally used char, either signed or unsigned, to represent bytes, but there is now a type that is not restricted to being either a character or an integer, although a byte can be converted to and from an integer. std::byte
is intended for interaction with storage and does not support arithmetic, although it does support bitwise operations.
The concept of a “variant” may be familiar to anyone who used or interacted with Visual Basic. A variant is a type-safe union, which at a given time holds a value of one of its alternative types (which can't be references, arrays or 'void').
Here's a simple example: suppose that we have some data where a person's age may be represented by an integer or by a string containing their date of birth. We could represent this using a variant containing an unsigned integer or a string. Assigning an integer to the variable sets the value, and you can then retrieve it using std::get
as follows:
std
::
variant
<
uint32_t
,
std
::
string
>
age
;
age
=
51
;
auto
a
=
std
::
get
<
uint32_t
>
(
age
);
Attempting to use a member that isn't set results in an exception being thrown:
try
{
std
::
cout
<<
std
::
get
<
std
::
string
>
(
age
)
<<
std
::
endl
;
}
catch
(
std
::
bad_variant_access
&
ex
)
{
std
::
cout
<<
"Doesn't contain a string"
<<
std
::
endl
;
}
Why use std::variant
rather than a plain union? The main reason is that unions are in the language mainly for compatibility with C, and don't work for non-POD types. This, among other things, means that unions can't easily have members that have user-written copy constructors and destructors. There are no such limitations on std::variant
.
std::optional
Another type, std::optional
, is amazingly useful, and mirrors what a number of functional languages provide. An 'optional
' is an object that may or may not hold a value, and is useful as the return value for a function when it can't return a value, as an alternative to, for example, returning a null pointer.
Using an optional
has the additional advantage that the possibility of failure is now made explicit in the function declaration, and since you have to extract the value from the optional
, there is much less likelihood of accidentally using a null value.
The following example defines a conversion function that tries to convert a string to an integer. By returning an optional
, the function builds in the possibility that an invalid string was passed and could not be converted. The caller uses the value_or
function to get the value out of the optional
, returning a default of zero if the conversion failed.
#include
using
namespace
std
::
experimental
;
optional
<
int
>
convert
(
const
std
::
string
&
s
)
{
try
{
int
res
=
std
::
stoi
(
s
);
return
res
;
}
catch
(
std
::
exception
&
)
{
return
{};
}
}
int
v
=
convert
(
"123"
).
value_or
(
0
);
std
::
cout
<<
v
<<
std
::
endl
;
int
v1
=
convert
(
"abc"
).
value_or
(
0
);
std
::
cout
<<
v1
<<
std
::
endl
;
std::any
Finally, we have std::any
, which provides a type-safe container for a single value of any type (provided that it is copy-constructible). You can check whether an any
contains a value, and use std::any_cast
to retrieve the value, as follows:
#include
using
namespace
std
::
experimental
;
std
::
vector
<
any
>
v
{
1
,
2.2
,
false
,
"hi!"
};
auto
&
t
=
v
[
1
].
type
();
// What is being held in this std::any?
if
(
t
==
typeid
(
double
))
std
::
cout
<<
"We have a double"
<<
"
\n
"
;
else
std
::
cout
<<
"We have a problem!"
<<
"
\n
"
;
std
::
cout
<<
any_cast
<
double
>
(
v
[
1
])
<<
std
::
endl
;
You can use the type()
member to get a type_info
object telling you what is being held in the any
. The types must match exactly, and you'll get a std::bad_any_cast
thrown if they don't:
try
{
std
::
cout
<<
any_cast
<
int
>
(
v
[
1
])
<<
std
::
endl
;
}
catch
(
std
::
bad_any_cast
&
)
{
std
::
cout
<<
"wrong type"
<<
std
::
endl
;
}
When might you use this data type? The simple answer is anywhere that you might have used a void*
pointer, but with type safety. For example, you might want different representations of an underlying value, such as '5' represented as an integer or a string. This kind of thing is common in interpreted languages, but it could also be useful where you want a representation that won't be subject to automatic conversion.
There's a lot more in C++17 than I've been able to describe here, and it is going to be worth every C++ developer's time to find out what's available.
Major compilers, including GCC, Clang, and MSVC, already support many of the changes; you can find out more details here.
There are several excellent summaries of the changes on the web that will provide you with a lot of information about the additions and changes. I would particularly mention those due to Tony van Eerd, a comprehensive StackOverflow article, and Bartek's excellent article.