Deduction Guides

Deduction guides are used for helping the compiler figure out what types go into the templates. Basically just rules on how to tie break collisions.

How to help the compiler help you

C++11 introduced the auto keyword which helped folks save a lot of time and energy manually specifying types in some places. Doing something like:

double pi = 3.14159;

could now be accomplished with:

auto two_pi = 6.283185; // we can figure out auto should really just be double!

// Just to confirm they are indeed the same type:
static_assert(std::is_same_v<decltype(pi), decltype(two_pi)>);

While it seems trivial for this type, auto really started to shine when we had crazier types involved. For example, when talking about iterators:

#include <vector>
#include <iostream>
#include <memory>

// We used to have to do something like:

{
    std::vector<std::pair<std::string, uint32_t>> people{std::make_pair("Kevin", 1), std::make_pair("Mike", 2), std::make_pair("Jenny", 3), std::make_pair("Laura", 4)};

    std::vector<std::pair<std::string, uint32_t>>::iterator people_iter = people.begin(); // YUCK!!!

    while (people_iter != people.end())
    {
        std::cout << people_iter->first << ", " << people_iter->second << std::endl;
        ++people_iter;
    }
}
Kevin, 1
Mike, 2
Jenny, 3
Laura, 4
{
    std::vector<std::pair<std::string, uint32_t>> people{ {"Kevin", 1}, {"Mike", 2}, {"Jenny", 3}, {"Laura", 4} };
    
    auto people_iter = people.begin(); // No more dealing with the explicit types!
    while (people_iter != people.end())
    {
        std::cout << people_iter->first << ", " << people_iter->second << std::endl;
        ++people_iter;
    }
}
Kevin, 1
Mike, 2
Jenny, 3
Laura, 4

When auto was introduced, it was a pretty big win when it came to readability. While overuse of anything is generally bad, auto let us escape the grunt work of having the exact type manually written out. In the same spirit of avoiding the extra work of having to manually specify things, CTAD was introduced. But before we talk about what it is and how it works, lets take a step back and talk about the world that existed before it.

Have you ever run across functions that look something like make_xxx where xxx was some type? These even exist in the standard library. A few common ones:

  • std::make_pair

  • std::make_tuple

  • std::make_unique

  • std::make_optional

This section is going to talk about why these were necessary, why they are no longer necessary starting C++17, and how you can also avoid using them (and more!).

So lets go through a simple example:

template <typename T, typename U>
struct MyPair
{
    MyPair(const T& t, const U& u) {}
};

// In the pre c++17 days, if we wanted to create an instance of MyPair, we'd have to do something like:
MyPair<int, double> my_pair{1, 2.0};
// and what this meant is that you had to know and declare the types of something upfront.
// But this was not that convenient. There is enough information on the right hand side for us to determine what the types are
// but the compiler not being able to figure it is unfortunate. In order to help out the compiler, people started to introduce the
// make_xxx style methods that gave the compiler enough information to realize what the types of the templates should be!

template <typename T, typename U>
inline MyPair<T, U> make_my_pair(const T& t, const U& u)
{
    return MyPair<T, U>(t, u);
}

// And then all was okay in the world and we could move on - we just had to use this make_xxx method now.
// So now people started writing:
auto my_other_pair = make_my_pair(1, 2.0);

While this is a simple example, you can take this a lot farther, and folks generally did. But this is a little cumbersome. It’s also not very efficient because now we are creating an extra function call and debugging this will be a pain. And why should we even have to define a seperate function anyway just so the constructor works out!

So C++17 introduced CTAD (Class Template Argument Deduction) that lets us gain a boost similar to the boost we gained when we had auto. Maybe we don’t have to explicitly specify all the types. We just need to nudge the compiler in the right direction with a guide!

So out went the old make_xxx and in came… well, the ability to write less code. Starting C++17, we can instead do:

MyPair my_cool_pair{1, 2.0}; // and the compiler will figure out that it's really just MyPair<int, double>

static_assert(std::is_same_v<decltype(my_cool_pair), decltype(my_pair)>);
#include <type_traits>

std::cout << typeid(my_cool_pair).name() << std::endl;
std::cout << typeid(my_pair).name() << std::endl;
N11__cling_N566MyPairIidEE
N11__cling_N566MyPairIidEE

Wait a second, we didn’t actually do anything. Why does this work? It’s because in trivial cases, the deduction guides come for free. Just to show you that this didn’t work in C++14, here is a side by side comparison: C++14 vs C++17 auto deduction in trivial cases

If there are any special cases that the trivial version doesn’t capture, then we need to implement our own deduction guides. The standard library folks were nice enough to implement them for corner cases for things like std::pair, std::tuple, etc. If you want to check them out, see: Deduction guide for std::pair

So it’s cool that it comes for free in some cases and the standard library implemented some, but how do we write our own?

Writing your own deduction guide

Lets start with a example we can play around with:

Gotchas to watch out for

Caveats

Let’s start with the more obvious ones:

We can’t empty initialize something since the deduction guides depend on a type

std::pair<int, int> my_empty_pair{};
std::cout << my_empty_pair.first << std::endl;
0

In the above example, we just initialized an empty instance of the pair<int, int> type and that was fine because the compiler had enough information (via our explicit expression) of what types are going into the pair. Once we start relying on the deduction guides, we can do:

std::pair my_auto_pair{1U, 2U};
std::cout << my_auto_pair.first << std::endl;
1

But no longer something like:

std::pair my_empty_pair_with_no_types{};

The compiler will rightfully puke on us with:

no viable constructor or deduction guide for deduction of template arguments of 'pair'

because there is no way for the compiler to know what we wanted in there! Heck, I don’t even know what I wanted in there.

All or nothing when it comes to specifying the types

Lets say we build a custom MyPair type

template <typename A, typename B>
struct MyPair
{
    MyPair(const A&, const B&) {};
};
MyPair one{1, 2.0}; // int, double
MyPair two{3, "4"}; // int, char

Out of luck when it comes to alias types

Pointer types can’t be deduced from a nullptr

Where they will fail