Book Image

C++20 STL Cookbook

By : Bill Weinman
Book Image

C++20 STL Cookbook

By: Bill Weinman

Overview of this book

Fast, efficient, and flexible, the C++ programming language has come a long way and is used in every area of the industry to solve many problems. The latest version C++20 will see programmers change the way they code as it brings a whole array of features enabling the quick deployment of applications. This book will get you up and running with using the STL in the best way possible. Beginning with new language features in C++20, this book will help you understand the language's mechanics and library features and offer insights into how they work. Unlike other books, the C++20 STL Cookbook takes an implementation-specific, problem-solution approach that will help you overcome hurdles quickly. You'll learn core STL concepts, such as containers, algorithms, utility classes, lambda expressions, iterators, and more, while working on real-world recipes. This book is a reference guide for using the C++ STL with its latest capabilities and exploring the cutting-edge features in functional programming and lambda expressions. By the end of the book C++20 book, you'll be able to leverage the latest C++ features and save time and effort while solving tasks elegantly using the STL.
Table of Contents (13 chapters)

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