3 Simple C++17 Features That Will Make Your Code Simpler
This article is a guest post written by guest author jft.
C++17 has brought a lot of features to the C++ language. Let’s dig into three of them that help make coding easier, more concise, intuitive and correct.
We’ll begin with Structured Bindings. These were introduced as a means to allow a single definition to define multiple variables with different types. Structured bindings apply to many situations, and we’ll see several cases where they can make code more concise and simpler.
Then we’ll see Template Argument Deduction, which allows us to remove template arguments that we’re used to typing, but that we really shouldn’t need to.
And we’ll finish with Selection Initialization, which gives us more control about object scoping and lets us define values where they belong.
So let’s start with structured bindings.
Structured Bindings
Structured Bindings allow us to define several objects in one go, in a more natural way than in the previous versions of C++.
From C++11 to C++17
This concept is not new in itself. Previously, it was always possible to return multiple values from a function and access them using std::tie
.
Consider the function:
std::tuple<char, int, bool> mytuple() { char a = 'a'; int i = 123; bool b = true; return std::make_tuple(a, i, b); }
This returns three variables all of different types. To access these from a calling function prior to C++17, we would need something like:
char a; int i; bool b; std::tie(a, i, b) = mytuple();
Where the variables have to be defined before use and the types known in advance.
But using Structured Bindings, we can simply do this as:
auto [a, i, b] = mytuple();
which is a much nicer syntax and is also consistent with modern C++ style using auto almost whenever possible.
So what can be used with a Structured Binding initialization? Basically anything that is a compound type – struct
, pair
and tuple
. Let’s see several cases where it can be useful.
Returning compound objects
This is the easy way to assign the individual parts of a compound type (such as a struct, pair etc) to different variables all in one go – and have the correct types automatically assigned. So let’s have a look at an example. If we insert into a map, then the result is a std::pair
:
std::map<char,int> mymap; auto mapret = mymap.insert(std::pair('a', 100));
And if anyone is wondering why the types are not explicitly stated for pair, then the answer is Template Argument Deduction in C++17 – keep reading!
So to determine if the insert was successful or not, we could extract the info from what the insert method returned:
The problem with this code is that a reader needs to look up what .second
is supposed to mean, if only mentally. But using Structured Bindings, this becomes:
auto [itelem, success] = mymap.insert(std::pair(’a’, 100)); If (!success) { // Insert failure }
Where itelem
is the iterator to the element and success is of type bool
, with true
for insertion success. The types of the variables are automatically deduced from the assignment – which is much more meaningful when reading code.
As a sneak peek into the last section, as C++17 now has Selection Initialization, then we could (and probably would) write this as:
if (auto [itelem, success] = mymap.insert(std::pair(‘a’, 100)); success) { // Insert success }
But more on this in a moment.
Iterating over a compound collection
Structured Bindings also work with range-for as well. So considering the previous mymap definition, prior to C++17 we would iterate it with code looking like this:
for (const auto& entry : mymap) { // Process key as entry.first // Process value as entry.second }
Or maybe, to be more explicit:
for (const auto& entry : mymap) { auto& key = entry.first; auto& value = entry.second; // Process entry }
But Structured Bindings allow us to write it more directly:
for (const auto&[key, value] : mymap) { // Process entry using key and value }
The usage of the variables key
and value
are more instructive than entry.first
and entry.second
– and without requiring the extra variable definitions.
Direct initialization
But as Structured Bindings can initialize from a tuple, pair etc, can we do direct initialization this way?
Yes we can. Consider:
auto a = ‘a’; auto i = 123; auto b = true;
which defines variables a
as type char with initial value ‘a’, i as type int with initial value 123 and b
as type bool with initial value true
.
Using Structured Bindings, this can be written as:
auto [a, i, b] = tuple(‘a’, 123, true); // With no types needed for the tuple!
This will define the variables a
, i
, b
the same as if the separate defines above had been used.
Is this really an improvement over the previous definition? OK, we’ve done in one line what would have taken three but why would we want to do this?
Consider the following code:
{ istringstream iss(head); for (string name; getline(iss, name); ) // Process name }
Both iss
and name
are only used within the for block, yet iss
has to be declared outside of the for statement and within its own block so that the scope is limited to that required.
This is weird, because iss belongs
to the for loop.
Initialization of multiple variables of the same type has always been possible. For example:
for (int i = 0, j = 100; i < 42; ++i, --j) { // Use i and j }
But what we’d like to write – but can’t – is:
for (int i = 0, char ch = ‘ ‘; i < 42; ++i) { // Does not compile // Use i and ch }
With Structured Bindings we can write:
for (auto[iss, name] = pair(istringstream(head), string {}); getline(iss, name); ) { // Process name }
and
for (auto[i, ch] = pair(0U, ‘ ‘); i < 42; ++i) { // The 0U makes i an unsigned int // Use i and ch }
Which allows the variables iss and name (and i
and ch
) to be defined within the scope of the for statement as needed and also their type to be automatically determined.
And likewise with the if
and switch
statements, which now take optional Selection Initialization in C++17 (see below). For example:
if (auto [a, b] = myfunc(); a < b) { // Process using a and b }
Note that we can’t do everything with structured bindings, and trying to fit them in to every situation can make the code more convoluted. Consider the following example:
if (auto [box, bit] = std::pair(std::stoul(p), boxes.begin()); (bit = boxes.find(box)) != boxes.end()){ // Process if using both box and bit variables }
Here variable box
is defined as type unsigned long and has an initial value returned from stoul(p)
. stoul()
, for those not familiar with it, is a <string>
function which takes a type std::string
as its first argument (there are other optional ones – including base) and parses its content as an integral number of the specified base (defaults to 10), which is returned as an unsigned long value.
The type of variable bit
is that of an iterator for boxes
and has an initial value of .begin()
– which is just to determine its type for auto. The actual value of variable bit
is set in the condition test part of the if statement. This highlights a constraint with using Structured Bindings in this way. What we really want to write is:
if (const auto [box, bit] = std::pair(std::stoul(p), boxes.find(box)); bit != boxes.end()){ // This doesn’t compile // Process if using both box and bit variables }
But we can’t because a variable declared within an auto
type specifier cannot appear within its own initializer! Which is kind of understandable.
So to sum up, the advantages of using Structured Bindings are:
- a single declaration that declares one or more local variables
- that can have different types
- whose types are always deduced using a single auto
- assigned from a composite type.
The drawback, of course, is that an intermediary (eg std::pair
) is used. This needn’t necessarily impact upon performance (it is only done once at the start of the loop anyhow) as move semantics would be used where possible – but note that where a type used is non-moveable (eg like std::array
) then this could incur a performance ‘hit’ depending upon what the copy operation involved.
But don’t pre-judge the compiler and pre-optimize code! If the performance isn’t as required, then use a profiler to find the bottleneck(s) – otherwise you are wasting development time. Just write the simplest / cleanest code that you can.
Template Argument Deduction
Put simply, Template Argument Deduction is the ability of templated classes to determine the type of the passed arguments for constructors without explicitly stating the type.
Before C++17, to construct an instance of a templated class we had to explicitly state the types of the argument (or use one of the make_xyz
support functions).
Consider:
std::pair<int, double> p(2, 4.5);
Here, p
is an instance of the class pair and is initialized with values of 2 and 4.5. Or the other method of achieving this would be:
auto p = std::make_pair(2, 4.5);
Both methods have their drawbacks. Creating “make functions” like std::make_pair
is confusing, artificial and inconsistent with how non-template classes are constructed. std::make_pair
, std::make_tuple
etc are available in the standard library, but for user-defined types it is worse: you have to write your own make_… functions. Doh!
Specifying template arguments, as in:
auto p = std::pair<int, double>(2, 4.5)
should be unnecessary since they can be inferred from the type of the arguments – as is usual with template functions.
In C++17, this requirement for specifying the types for a templated class constructor has been abolished. This means that we can now write:
auto p = std::pair(2, 4.5);
or
std::pair p(2, 4.5);
which is the logical way you would expect to be able to define p
!
So considering the earlier function mytuple()
. Using Template Argument Deduction (and auto for function return type), consider:
auto mytuple() { char a = 'a'; int i = 123; bool b = true; return std::tuple(a, i, b); // No types needed }
This is a much cleaner way of coding – and in this case we could even wrap it as:
auto mytuple() { return std::tuple(‘a’, 123, true); // Auto type deduction from arguments }
There is more to it than that, and to dig deeper into that feature you can check out Simon Brand’s presentation about Template Argument Deduction.
Selection Initialization
Selection Initialization allows for optional variable initialization within if
and switch
statements – similar to that used within for statements. Consider:
for (int a = 0; a < 10; ++a) { // for body }
Here the scope of a
is limited to the for statement. But consider:
{ auto a = getval(); if (a < 10) { // Use a } }
Here variable a
is used only within the if statement but has to be defined outside within its own block if we want to limit its scope. But in C++17 this can be written as:
if (auto a = getval(); a < 10) { // Use a }
Which follows the same initialization syntax as the for statement – with the initialization part separated from the selection part by a semicolon (;
). This same initialization syntax can similarly be used with the switch statement. Consider:
switch (auto ch = getnext(); ch) { // case statements as needed }
Which all nicely helps C++ to be more concise, intuitive and correct! How many of us have written code such as:
int a; if ((a = getval()) < 10) { // Use a } ... // Much further on in the code – a has the same value as previously if (a == b) { //... }
Where a
before the second if
hasn’t been initialized properly before the test (an error) but isn’t picked up by the compiler because of the earlier definition – which is still in scope as it isn’t defined within its own block. If this had been coded in C++17 as:
if (auto a = getval(); a < 10) { // Use a } ... // Much further on in the code - a is not now defined if (a == b) { // ... }
Then this would have been picked up by the compiler and reported as an error. A compiler error costs much less to fix than an unknown run-time problem!
C++17 helps making code simpler
In summary, we’ve seen how Structured Bindings allow for a single declaration that declares one or more local variables that can have different types, and whose types are always deduced using a single auto
. They can be assigned from a composite type.
Template Argument Deduction allows us to avoid writing redundant template parameters and helper functions to deduce them. And Selection Initialization make the initialization in if and switch statements consistent with the one in for statements – and avoids the pitfall of variable scoping being too large.
References
Structured Bindings:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0144r2.pdf
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0217r3.html
Template Argument Deduction:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0091r3.html
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0433r2.html
Selection Initialization:
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0305r1.html
You may also like
Don't want to miss out ? Follow:   Share this post!