Language Features

How to Avoid Template Type Deduction in C++

Illustrated by Zara Magumyan

Template type deduction is an awesome feature of C++. Except when it gets in your way by not doing what you want.
Based on an example of a function that changes the keys of the elements of an std::map, we see how to prevent template type deduction to kick in for selected function parameters.

Motivating Example: Changing the Keys of a Map or Set

Here is how to change a key of the elements of a std::map:

auto myMap = std::map<std::string, int>{ {"one", 1}, {"two", 2}, {"three", 3} };

auto node = myMap.extract("two");
if (!node.empty())
{
    node.key() = "dos";
    myMap.insert(std::move(node));
}

This relies on the C++17 extract method of std::map.
And std::set has a similar way to perform the operation:

auto mySet = std::set<std::string>{"one", "two", "three"};

auto node = mySet.extract("two");
if(!node.empty())
{
    node.value() = "dos";
    mySet.insert(std::move(node));
}

But you don't want to write all this in your code. It would be nicer to have a function that encapsulates the mechanics of taking a node off the container and putting it back in, like this:

auto myMap = std::map<std::string, int>{ {"one", 1}, {"two", 2}, {"three", 3} };
replace_key(myMap, "two", "dos");

auto mySet = std::set<std::string>{"one", "two", "three"};
replace_key(mySet, "two", "dos");

At first sight, this looks really simple because std::map and std::set seem to have the same code to extract a node and to put it back in. So we could have a generic function that takes a template parameter type for the container and be done with it.
Except they don't have the same code. They almost have the same code, except for the method to change the value of the extracted node. If you look at line 6 in both of the above snippets, you'll see that the one for std::map uses node.key() and the one for std::set uses node.value().
This difference can be surprising, especially since the values of sets are often themselves called "keys" as in key_type, which is an alias for value_type in std::set.
So we need to have two implementations for replace_key.

One way out of this issue is to call the function replace_value for std::set. Then we'd have two independent functions with two implementations. Until we decide to implement replace_value for std::vectors too, and we're back to the need of having two overloads of a function that has different code for different containers.
In any case, I think it is interesting to see how to overload the function for several containers, because it is not trivial. In particular, it shows us how to deactivate the template type deduction of selected function parameters.

The Problem of Template Type Deduction

What's all that fuss with overloading anyway? Let's just write the overloads like this:

template<typename Key, typename Value>
void replace_key(std::map<Key, Value>& container, // this is the overload for maps
                const Key& oldKey,
                const Key& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.key() = newKey;
        container.insert(std::move(node));
    }
}

template<typename Key>
void replace_key(std::set<Key>& container, // this is the overload for sets
                const Key& oldKey,
                const Key& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.value() = newKey;
        container.insert(std::move(node));
    }
}

Let's now test them with the following calling code, for std::map to start:

auto myMap = std::map<std::string, int>{ {"one", 1}, {"two", 2}, {"three", 3} };
replace_key(myMap, "two", "dos");

What we get is a compilation error:

main.cpp: In function 'int main()':
main.cpp:35:32: error: no matching function for call to 'replace_key(std::map<std::__cxx11::basic_string<char>, int>&, const char [4], const char [4])'
 replace_key(myMap, "two", "dos");
                                ^
main.cpp:7:6: note: candidate: 'template<class Key, class Value> void replace_key(std::map<Key, Value>&, const Key&, const Key&)'
 void replace_key(std::map<Key, Value>& container,
      ^~~~~~~~~~~
main.cpp:7:6: note:   template argument deduction/substitution failed:
main.cpp:35:32: note:   deduced conflicting types for parameter 'const Key' ('std::__cxx11::basic_string<char>' and 'char [4]')
 replace_key(myMap, "two", "dos");
                                ^
main.cpp:20:6: note: candidate: 'template<class Key> void replace_key(std::set<Key>&, const Key&, const Key&)'
 void replace_key(std::set<Key>& container,
      ^~~~~~~~~~~
main.cpp:20:6: note:   template argument deduction/substitution failed:
main.cpp:35:32: note:   'std::map<std::__cxx11::basic_string<char>, int>' is not derived from 'std::set<Key>'
 replace_key(myMap, "two", "dos");
                                ^

The problem is that "two" and "dos" are of type char const *, which is implicitly convertible to std::string. But there is no implicit conversion in the context of template type deduction (like we saw when handling multiple types with std::max). So Key is deduced as being of type char const *.
On the other hand, Container is deduced to be of type std::map<std::string, int> because this is the type of myMap. Because of this inconsistency in the deduced types of Key, the template generation fails, so the compiler doesn't find a replace_key function.
Note that we wouldn't have encountered this problem if we didn't rely on an implicit conversion—for example, if we had a std::map<int, int> or even a std::map<int, std::string>. But our generic function has to work with all types.
One way out is to work around the implicit conversion:

auto myMap = std::map<std::string, int>{ {"one", 1}, {"two", 2}, {"three", 3} };
replace_key(myMap, std::string("two"), std::string("dos"));

Yuck! This doesn't look natural at all. Can't we do better?

Using type_identity

C++20 will define a new template type in the standard library: std::type_identity. This is the identity function in template metaprogramming. Or put another way, this is the template type defined by

template<typename T>
struct type_identity
{
    using type = T;
};

It comes with C++20's standard library, but it was implementable since C++98 (by using a typedef instead of using).
Like all metaprogramming functions, std::type_identity comes with its _t counterpart, to avoid the noisy typename and ::type in calling code:

template< class T >
using type_identity_t = typename type_identity<T>::type;

We can use type_identity to prevent the deduction of some template parameters (thanks to Walletfox for showing me this technique!):

template<typename Key, typename Value>
void replace_key(std::map<Key, Value>& container,
                 const type_identity_t<Key>& oldKey,
                 const type_identity_t<Key>& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.key() = newKey;
        container.insert(std::move(node));
    }
}

What does this change?

First off, once we know Key we can deduce type_identity_t<Key>: it is Key, by definition. But what makes it useful is that the deduction doesn't work the other way around: if the compiler knows Key, it doesn't try to deduce type_identity_t<Key>.
Indeed, there could be template specializations of type_identity that define type as something different. We could even have several specializations of type_identity that define type as the same thing. So you can't deduce type_identity<Key> if you only know Key.
As a result, type_identity_t<Key> doesn't take part in the deduction of template parameters in replace_key. Only Container does, and this is just enough for us because it allows us to deduce Key and Value.
This technique also works with std::set:

template<typename Key>
void replace_key(std::set<Key>& container,
                 const type_identity_t<Key>& oldKey,
                 const type_identity_t<Key>& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.value() = newKey;
        container.insert(std::move(node));
    }
}

A Curious Name

What we use type_identity for is to prevent type deduction. But this is not clear in the naming of our interface. To make our intention clearer in templates, we could use an alias for type_identity_t:

template< class T >
using not_deduced = type_identity_t<T>;

template<typename Key, typename Value>
void replace_key(std::map<Key, Value>& container,
                 const not_deduced<Key>& oldKey,
                 const not_deduced<Key>& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.key() = newKey;
        container.insert(std::move(node));
    }
}

template<typename Key>
void replace_key(std::set<Key>& container,
                 const not_deduced<Key>& oldKey,
                 const not_deduced<Key>& newKey)
{
    auto node = container.extract(oldKey);
    if(!node.empty())
    {
        node.value() = newKey;
        container.insert(std::move(node));
    }
}

There are advantages and drawbacks to doing this. not_deduced is not a standard name, so a reader of your code won't know it. It has to be clear enough for them to understand it means that Key won't be used for type deduction.
On the other hand, if they don't know the technique of using type_identity to avoid type deduction, the code with type_identity_t in the interface will raise more than one eyebrow.
If anything, it will help you later remember why you didn't just use Key in the interface.

What's your opinion? Should we use an alias for type_identity_t in this context? If so, what alias would you think make the intention clearer?
You can let me know in the comments section as well as let me know any other piece of feedback you have on this post or anything else on Fluent C++.

Jonathan Boccara

author

Jonathan Boccara is a C++ software engineering lead, blogger and writer focusing on how to make code expressive. His blog is Fluent C++.

Zara Magumyan

illustrator

Zara Magumyan is an illustrator and graphic designer based in Yerevan, Armenia. She has designed for various industries including branding, IT, entertainment, and gaming. Currently specializes in illustration, user interface development, and digital products.