Chapter 2: General STL Features
This chapter is a general potpourri of STL features and techniques. These are mostly new features introduced over the past few years, which may not yet be widely used. These are useful techniques that will improve the simplicity and readability of your code.
In this chapter we will cover the following recipes:
- Use the new
span
class to make your C-arrays safer - Use structured binding to return multiple values
- Initialize variables within
if
andswitch
statements - Use template argument deduction for simplicity and clarity
- Use
if constexpr
to simplify compile-time decisions
Technical requirements
You can find the code for this chapter on GitHub at https://github.com/PacktPublishing/CPP-20-STL-Cookbook/tree/main/chap02.
Use the new span class to make your C-arrays safer
New for C++20, the std::span
class is a simple wrapper that creates a view over a contiguous sequence of objects. The span
doesn't own any of its own data, it refers to the data in the underlying structure. Think of it as string_view
for C-arrays. The underlying structure may be a C-array, a vector
, or an STL array
.
How to do it…
You can create a span from any compatible contiguous-storage structure. The most common use case will involve a C-array. For example, if you try to pass a C-array directly to a function, the array is demoted to a pointer and the function has no easy way to know the size of the array:
void parray(int * a); // loses size information
If you define your function with a span
parameter, you can pass it a C-array and it will be promoted to span
. Here's a template function that takes a span
and prints out the size in elements and in bytes:
template<typename T> void pspan(span<T> s) { cout << format("number of elements: {}\n", s.size()); cout << format("size of span: {}\n", s.size_bytes()); for(auto e : s) cout << format("{} ", e); cout << "\n"; }
You can pass a C-array to this function and it's automatically promoted to span
:
int main() { int carray[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; pspan<int>(carray); }
Output:
number of elements: 10 number of bytes: 40 1 2 3 4 5 6 7 8 9 10
The purpose of span
is to encapsulate the raw data to provide a measure of safety and utility, with a minimum of overhead.
How it works…
The span
class itself doesn't own any data. The data belongs to the underlying data structure. The span
is essentially a view over the underlying data. It also provides some useful member functions.
Defined in the <span>
header, the span
class looks something like:
template<typename T, size_t Extent = std::dynamic_extent> class span { T * data; size_t count; public: ... };
The Extent
parameter is a constant of type constexpr size_t
, which is computed at compile time. It's either the number of elements in the underlying data or the std:: dynamic_extent
constant, which indicates that the size is variable. This allows span
to use an underlying structure like a vector
, which may not always be the same size.
All member functions are constexpr
and const
qualified. Member functions include:

Important Note
The span
class is but a simple wrapper that performs no bounds checking. So, if you try to access element n+1 in a span
of n elements, the result is undefined, which is tech for, "Bad. Don't do that."
Use structured binding to return multiple values
Structured binding makes it easy to unpack the values of a structure into separate variables, improving the readability of your code.
With structured binding you can directly assign the member values to variables like this:
things_pair<int,int> { 47, 9 }; auto [this, that] = things_pair; cout << format("{} {}\n", this, that);
Output:
47 9
How to do it…
- Structured binding works with
pair
,tuple
,array
, andstruct
. Beginning with C++20, this includes bit-fields. This example uses a C-array:int nums[] { 1, 2, 3, 4, 5 }; auto [ a, b, c, d, e ] = nums; cout << format("{} {} {} {} {}\n", a, b, c, d, e);
Output:
1 2 3 4 5
Because the structured binding uses automatic type deduction, its type must be auto
. The names of the individual variables are within the square brackets, [ a, b, c, d, e ]
.
In this example the int
C-array nums
holds five values. These five values are assigned to the variables (a
, b
, c
, d
, and e
) using structured binding.
- This also works with an STL
array
object:array<int,5> nums { 1, 2, 3, 4, 5 }; auto [ a, b, c, d, e ] = nums; cout << format("{} {} {} {} {}\n", a, b, c, d, e);
Output:
1 2 3 4 5
- Or you can use it with a
tuple
:tuple<int, double, string> nums{ 1, 2.7, "three" }; auto [ a, b, c ] = nums; cout << format("{} {} {}\n", a, b, c);
Output:
1 2.7 three
- When you use it with a
struct
it will take the variables in the order they're defined:struct Things { int i{}; double d{}; string s{}; }; Things nums{ 1, 2.7, "three" }; auto [ a, b, c ] = nums; cout << format("{} {} {}\n", a, b, c);
Output:
1 2.7 three
- You can use a reference with a structured binding, which allows you to modify the values in the bound container, while avoiding duplication of the data:
array<int,5> nums { 1, 2, 3, 4, 5 }; auto& [ a, b, c, d, e ] = nums; cout << format("{} {}\n", nums[2], c); c = 47; cout << format("{} {}\n", nums[2], c);
Output:
3 3 47 47
Because the variables are bound as a reference, you can assign a value to c
and it will change the value in the array as well (nums[2]
).
- You can declare the array
const
to prevent values from being changed:const array<int,5> nums { 1, 2, 3, 4, 5 }; auto& [ a, b, c, d, e ] = nums; c = 47; // this is now an error
Or you can declare the binding const
for the same effect, while allowing the array to be changed elsewhere and still avoid copying data:
array<int,5> nums { 1, 2, 3, 4, 5 }; const auto& [ a, b, c, d, e ] = nums; c = 47; // this is also an error
How it works…
Structured binding uses automatic type deduction to unpack the structure into your variables. It determines the type of each value independently, and assigns a corresponding type to each variable.
- Because structured binding uses automatic type deduction, you cannot specify a type for the binding. You must use
auto
. You should get a reasonable error message if you try to use a type for the binding:array<int,5> nums { 1, 2, 3, 4, 5 }; int [ a, b, c, d, e ] = nums;
Output:
error: structured binding declaration cannot have type 'int' note: type must be cv-qualified 'auto' or reference to cv-qualified 'auto'
Above is the error from GCC when I try to use int
with the structured binding declaration.
- It's common to use structured binding for a return type from a function:
struct div_result { long quo; long rem; }; div_result int_div(const long & num, const long & denom) { struct div_result r{}; r.quo = num / denom; r.rem = num % denom; return r; } int main() { auto [quo, rem] = int_div(47, 5); cout << format("quotient: {}, remainder {}\n", quo, rem); }
Output:
quotient: 9, remainder 2
- Because the
map
container classes return a pair for each element, it can be convenient to use structured binding to retrieve key/value pairs:map<string, uint64_t> inhabitants { { "humans", 7000000000 }, { "pokemon", 17863376 }, { "klingons", 24246291 }, { "cats", 1086881528 } }; // I like commas string make_commas(const uint64_t num) { string s{ std::to_string(num) }; for(int l = s.length() - 3; l > 0; l -= 3) { s.insert(l, ","); } return s; } int main() { for(const auto & [creature, pop] : inhabitants) { cout << format("there are {} {}\n", make_commas(pop), creature); } }
Output:
there are 1,086,881,528 cats there are 7,000,000,000 humans there are 24,246,291 klingons there are 17,863,376 pokemon
Using structured binding to unpack structures should make your code clearer and easier to maintain.
Initialize variables within if and switch statements
Beginning with C++17, if
and switch
now have initialization syntax, much like the for
loop has had since C99. This allows you to limit the scope of variables used within the condition.
How to do it…
You may be accustomed to code like this:
const string artist{ "Jimi Hendrix" }; size_t pos{ artist.find("Jimi") }; if(pos != string::npos) { cout << "found\n"; } else { cout << "not found\n"; }
This leaves the variable pos
exposed outside the scope of the conditional statement, where it needs to be managed, or it can collide with other attempts to use the same symbol.
Now you can put the initialization expression inside the if
condition:
if(size_t pos{ artist.find("Jimi") }; pos != string::npos) { cout << "found\n"; } else { cout << "not found\n"; }
Now the scope of the pos
variable is confined to the scope of the conditional. This keeps your namespace clean and manageable.
How it works…
The initializer expression can be used in either if
or switch
statements. Here are some examples of each.
- Use an initializer expression with an
if
statement:if(auto var{ init_value }; condition) { // var is visible } else { // var is visible } // var is NOT visible
The variable defined in the initializer expression is visible within the scope of the entire if
statement, including the else
clause. Once control flows out of the if
statement scope, the variable will no longer be visible, and any relevant destructors will be called.
- Use an initializer expression with a
switch
statement:switch(auto var{ init_value }; var) { case 1: ... case 2: ... case 3: ... ... Default: ... } // var is NOT visible
The variable defined in the initializer expression is visible within the scope of the entire switch
statement, including all the case
clauses and the default
clause, if included. Once control flows out of the switch
statement scope, the variable will no longer be visible, and any relevant destructors will be called.
There's more…
One interesting use case is to limit the scope of a lock_guard
that's locking a mutex. This becomes simple with an initializer expression:
if (lock_guard<mutex> lg{ my_mutex };
condition) {
// interesting things happen here
}
The lock_guard
locks the mutex in its constructor and unlocks it in its destructor. Now the lock_guard
will be automatically destroyed when it runs out of the scope of the if
statement. In the past you would have had to delete it or enclose the whole if
statement in an extra block of braces.
Another use case could be using a legacy interface that uses output parameters, like this one from SQLite:
if( sqlite3_stmt** stmt, auto rc = sqlite3_prepare_v2(db, sql, -1, &_stmt, nullptr); !rc) { // do SQL things } else { // handle the error // use the error code return 0; }
Here I can keep the statement handle and the error code localized to the scope of the if
statement. Otherwise, I would need to manage those objects globally.
Using initializer expressions will help keep your code tight and uncluttered, more compact, and easier to read. Refactoring and managing your code will also become easier.
Use template argument deduction for simplicity and clarity
Template argument deduction occurs when the types of the arguments to a template function, or class template constructor (beginning with C++17), are clear enough to be understood by the compiler without the use of template arguments. There are certain rules to this feature, but it's mostly intuitive.
How to do it…
In general, template argument deduction happens automatically when you use a template with clearly compatible arguments. Let's consider some examples.
- In a function template, argument deduction usually looks something like this:
template<typename T> const char * f(const T a) { return typeid(T).name(); } int main() { cout << format("T is {}\n", f(47)); cout << format("T is {}\n", f(47L)); cout << format("T is {}\n", f(47.0)); cout << format("T is {}\n", f("47")); cout << format("T is {}\n", f("47"s)); }
Output:
T is int T is long T is double T is char const * T is class std::basic_string<char...
Because the types are easily discernable there is no reason to specify a template parameter like f<int>(47)
in the function call. The compiler can deduce the <int>
type from the argument.
Note
The above output shows meaningful type names where most compilers will use shorthand, like i
for int
and PKc
for const char *
, and so on.
- This works just as well for multiple template parameters:
template<typename T1, typename T2> string f(const T1 a, const T2 b) { return format("{} {}", typeid(T1).name(), typeid(T2).name()); } int main() { cout << format("T1 T2: {}\n", f(47, 47L)); cout << format("T1 T2: {}\n", f(47L, 47.0)); cout << format("T1 T2: {}\n", f(47.0, "47")); }
Output:
T1 T2: int long T1 T2: long double T1 T2: double char const *
Here the compiler is deducing types for both T1
and T2
.
- Notice that the types must be compatible with the template. For example, you cannot take a reference from a literal:
template<typename T> const char * f(const T& a) { return typeid(T).name(); } int main() { int x{47}; f(47); // this will not compile f(x); // but this will }
- Beginning with C++17 you can also use template parameter deduction with classes. So now this will work:
pair p(47, 47.0); // deduces to pair<int, double> tuple t(9, 17, 2.5); // deduces to tuple<int, int, double>
This eliminates the need for std::make_pair()
and std::make_tuple()
as you can now initialize these classes directly without the explicit template parameters. The std::make_*
helper functions will remain available for backward compatibility.
How it works…
Let's define a class so we can see how this works:
template<typename T1, typename T2, typename T3> class Thing { T1 v1{}; T2 v2{}; T3 v3{}; public: explicit Thing(T1 p1, T2 p2, T3 p3) : v1{p1}, v2{p2}, v3{p3} {} string print() { return format("{}, {}, {}\n", typeid(v1).name(), typeid(v2).name(), typeid(v3).name() ); } };
This is a template class with three types and three corresponding data members. It has a print()
function, which returns a formatted string with the three type names.
Without template parameter deduction, I would have to instantiate an object of this type like this:
Things<int, double, string> thing1{1, 47.0, "three" }
Now I can do it like this:
Things thing1{1, 47.0, "three" }
This is both simpler and less error prone.
When I call the print()
function on the thing1
object, I get this result:
cout << thing1.print();
Output:
int, double, char const *
Of course, your compiler may report something effectively similar.
Before C++17, template parameter deduction didn't apply to classes, so you needed a helper function, which may have looked like this:
template<typename T1, typename T2, typename T3> Things<T1, T2, T3> make_things(T1 p1, T2 p2, T3 p3) { return Things<T1, T2, T3>(p1, p2, p3); } ... auto thing1(make_things(1, 47.0, "three")); cout << thing1.print();
Output:
int, double, char const *
The STL includes a few of these helper functions, like make_pair()
and make_tuple()
, etc. These are now obsolescent, but will be maintained for compatibility with older code.
There's more…
Consider the case of a constructor with a parameter pack:
template <typename T> class Sum { T v{}; public: template <typename... Ts> Sum(Ts&& ... values) : v{ (values + ...) } {} const T& value() const { return v; } };
Notice the fold expression in the constructor (values + ...)
. This is a C++17 feature that applies an operator to all the members of a parameter pack. In this case, it initializes v
to the sum of the parameter pack.
The constructor for this class accepts an arbitrary number of parameters, where each parameter may be a different class. For example, I could call it like this:
Sum s1 { 1u, 2.0, 3, 4.0f }; // unsigned, double, int, // float Sum s2 { "abc"s, "def" }; // std::sring, c-string
This, of course, doesn't compile. The template argument deduction fails to find a common type for all those different parameters. We get an error message to the effect of:
cannot deduce template arguments for 'Sum'
We can fix this with a template deduction guide. A deduction guide is a helper pattern to assist the compiler with a complex deduction. Here's a guide for our constructor:
template <typename... Ts> Sum(Ts&& ... ts) -> Sum<std::common_type_t<Ts...>>;
This tells the compiler to use the std::common_type_t
trait, which attempts to find a common type for all the parameters in the pack. Now our argument deduction works and we can see what types it settled on:
Sum s1 { 1u, 2.0, 3, 4.0f }; // unsigned, double, int, // float Sum s2 { "abc"s, "def" }; // std::sring, c-string auto v1 = s1.value(); auto v2 = s2.value(); cout << format("s1 is {} {}, s2 is {} {}", typeid(v1).name(), v1, typeid(v2).name(), v2);
Output:
s1 is double 10, s2 is class std::string abcdef
Use if constexpr to simplify compile-time decisions
An if constexpr(
condition)
statement is used where code needs to be executed based on a compile-time condition. The condition may be any constexpr
expression of type bool
.
How to do it…
Consider the case where you have a template function that needs to operate differently depending upon the type of the template parameter.
template<typename T> auto value_of(const T v) { if constexpr (std::is_pointer_v<T>) { return *v; // dereference the pointer } else { return v; // return the value } } int main() { int x{47}; int* y{&x}; cout << format("value is {}\n", value_of(x)); // value cout << format("value is {}\n", value_of(y)); // pointer return 0; }
Output:
value is 47 value is 47
The type of the template parameter T
is available at compile time. The constexpr if
statement allows the code to easily distinguish between a pointer and a value.
How it works…
The constexpr if
statement works like a normal if
statement except it's evaluated at compile time. The runtime code will not contain any branch statements from a constexpr if
statement. Consider our branch statement from above:
if constexpr (std::is_pointer_v<T>) { return *v; // dereference the pointer } else { return v; // return the value }
The condition is_pointer_v<T>
tests a template parameter, which is not available at runtime. The constexpr
keyword tells the compiler that this if
statement needs to evaluate at compile time, while the template parameter <T>
is available.
This should make a lot of meta programming situations much easier. The if constexpr
statement is available in C++17 and later.