This article is from my old blog at kacperkolodziej.com.
In 2017 a new standard of C++ language has been released. Unlike C++14, C++17 has introduced a lot of new features. C++14 was rather a supplement for C++11 (ok, it has introduced some completely new features, but most were improvements of what we had known from C++11). C++17 brought a lot of new possibilities.
In this post I'm going to tell you about new things in C++17 which I like the most, because I found them useful in my newest projects.
Structured bindings
Everyone who has ever used std::tuple in C++ and tuples builtin into Python
can obviously see the difference between approach to this structure in both
languages. Python supports tuples as a part of language syntax. C++ has
special type in standard library. For me it was always much easier to decide
on tuple in Python than in C++. One cool thing about tuples in Python is
extracting their values:
b = 2, "string", 42 # tuple with three elements of different types
x, y, z = b # x, y, z are now elements of tuple b
b[1] # extracts second element of tuple b
In C++ before release of C++17 standard there were two ways of doing it:
auto a = std::make_tuple(1, 2, "string");
int x, y;
std::string z;
std::tie(x, y, z) = a; // extracting tuple's values to variables
std::get<1>(a); // extracting second element
We can easily see that in "old" C++ (C++14) it was not very comfortable to
extract data from tuple. Programmers often prefered using own simple structures
to use std::tuple type.
What has changed in C++17? The answer is structured bindings. Structured bindings has been described as decomposition in P0144R0. Later, in P0615R0 its name was changed to structured bindings. Syntax has been stated in P0217R3.
Making long story short, new C++ syntax element lets us to make declaration and extract data from tuple into newly declared variables. This is how it looks like:
auto a = std::make_tuple(1, 2, "string");
auto [x, y, z] = a; // structured bindings
What is great about structured bindings is that we can make our own types
support it. It's not complicated, because we have to create specialization for
some types from std namespace: struct tuple_size, struct tuple_element and function get.
I hope that tuples will be seen in C++ code more often when compilers versions supporting C++17 will become more common.
Fold expressions
Have you ever used variadic templates? Using template parameters' pack in most cases required recursive calls. It's not bad solution, but in my opinion not sufficient for such powerful language. Fold expressions is a language tool which lets us accumulate values of template parameters pack using binary operator. There are four variants of fold expression:
- unary right fold
- unary left fold
- binary right fold
- binary left fold
In first and second one parameter pack and operator is used. In third and fourth
we can also add init value. On the listing below you can see source code
example in which unary right fold is used in makeValue function. On the
other hand, makeValueOld is using recursive approach to unpack parameters.
#include <cstdint>
// C++17 - with fold expression
template <typename... Args>
uint32_t makeValue(Args... args) {
return (args ^ ...);
}
// before C++17 - with recursion
uint32_t makeValueOld() {
return 0;
}
template <typename First, typename... Args>
uint32_t makeValueOld(First f, Args... args) {
return f ^ makeValueOld(args...);
}
// usage
int main() {
volatile int x = makeValue(1, 1 << 1, 1 << 2, 1 << 3);
volatile int y = makeValueOld(1, 1 << 1, 1 << 2, 1 << 3);
return 0;
}
I have compiled this code with Linaro's GCC 7.2.1 for ARM. Without
optimization, version with fold expression has all operations inline. Old
style function has recursive calls. In case of -O2 optimization GCC
executes both these functions at compile time.
What I like very much about new C++ standards are constexpr. In C++17 they
became much more powerful than in C++14. Defining both makeValue and makeValueOld as
constexpr functions results in just two constant stores in ASM:
movs r3, #15
str r3, [r7, #4]
movs r3, #15
str r3, [r7]
So if you use constexpr both approaches are equally efficient, because
they are executed at compile time. However, version with fold expression looks
much clearer in my opinion.
Binary left and right folds let us to use initial value to which all parameters will be accumulated. It can be used to call overloaded operator on an object:
template <typename... Args>
constexpr void output(std::ostream& stream, Args&&... args) {
(stream << ... << args) << std::endl;
}
Difference between right and left folds is in order in which operations are
applied. In right folds parameters are accumulated from last (right-most) to
first (left-most). In left folds order is reverse. In output function
binary left fold has been used.
You can read more about folds at cppreference.com and N4295 paper.
Constexpr if conditions
Next improvement which I have found interesting is constexpr if. In fact
these keywords are now used in different order, but this is the former syntax
which is used as this feature's name ([stmt.if] in N4659).
Expressions marked with constexpr can be executed during compilation, but
don't have to. It means that we can write code which will be executed during
compilation only when all necessary data is available then.
What does if constexpr (P0292R2) mean? It informs
compiler that condition check must be performed at compile time.
What programmer gains? Depending on the condition's value, some piece of code can be removed. Suppose we have such function:
constexpr int x = 2;
int f(int a, int b) {
if constexpr (x & 1) {
return a * b;
} else {
return a + b;
}
}
Removing register saving and loading instructions, we receive this assembly code (again it's ARM ASM generated with Linaro GCC 7.2.1):
ldr r2, [r7, #4]
ldr r3, [r7]
add r3, r3, r2
mov r0, r3
There are no branches (jumps), multiplications and other instructions that
should be present if if wasn't a constexpr. I like constexpr
if because it will let me to avoid using macros which are hard to debug.
Initialization statements in selection statements
Awareness of object's lifetime scopes often effects in such constructions:
{
std::fstream f{"some_file"};
if (f.is_open()) {
// do sth
}
}
// some_file has been already closed, f object destroyed and memory freed
This approach is absolutely correct. We can easily see where object of
std::fstream class is closed and destroyed. We also can be sure that
this piece of code will not leak memory if caught exception would occur.
New syntax of if and switch statements takes great thing from
for loop construction: init phase. Code sample presented above can
now be written this way:
if (std::fstream f{"some_file"}; f.is_open()) {
// do sth
}
// here f doesn't exist
Functionally it is the same as the example above, but it is shorter, doesn't require additional indentation level and is more intuitive (for those who knows new syntax ;-)). Here is proposal document containing whole description: P0305R1.
Nested namespaces
Nested namespaces were always aesthetic problem for me. I used to define them this way:
namespace A {
namespace B {
int i;
} } // namespace A::B
C++17 will help me to do it better:
namespace A::B {
int i;
} // namespace A::B
For me it's meaningful improvement, because it makes code more readable. Nothing more, but it's enough. All formal things are in N4230 paper.
Others
These few changes are definitely my favourite ones. However C++17 introduced
more interesting things. It is worth to mention std::byte
definition. I used to use uint8_t as a byte representation in
byte-oriented memory accesses, but it is not elegant solution. New type
definition will now tell anyone reading code that in this place we are operating
on bare memory - neither on integers nor characters (P0298R0). Other small
change is removing register keyword which has never meant anything
concrete (P0001R1). It was just hint for compiler to keep given variable in
registers, but was never really respected. There are also other novelties which
I think are really significant, but I have found the most useful these mentioned
in this post.