Discussion: A Specialized String Theory
Execute the code to understand the output and gain insights into template specialization and overload resolution.
Run the code
Now, it’s time to execute the code and observe the output.
#include <iostream>template<typename T>void serialize(T&) { std::cout << "template\n"; } // 1template<>void serialize<>(const std::string&) { std::cout << "specialization\n"; } // 2void serialize(const std::string&) { std::cout << "normal function\n"; } // 3int main(){std::string hello_world{"Hello, world!"};serialize(hello_world);serialize(std::string{"Good bye, world!"});}
Understanding the output
Remember our serialization library from the String Theory puzzle? In the meantime, a C++ expert has come along and decided that everything is better with templates. Whether or not that’s true is another discussion, but it certainly makes overload resolution more interesting!
We find three definitions of serialize
in the above code puzzle:
Line 4: This is the primary function template for
serialize()
.Line 7: This is an explicit specialization of this template for
std::string
. We can recognize it as a specialization by the emptytemplate<>
tag.Line 9: Finally, we see a plain old non-template
serialize
function.
Function template and overload resolution
Since the name serialize
is overloaded, when we write a call to that name, overload resolution needs to work out which one to call. As we learned in the “String Theory” puzzle, it does that by creating a set of all candidate functions of that name and then selecting the best viable function from that set. It’s clear that the normal, non-template serialize
function is a candidate, but what happens with the function template? And how is the compiler able to disambiguate between the function template specialization and the normal function, which both have the same signature?
When a function template is involved in overload resolution, the compiler looks at all the primary templates of that name. It doesn’t consider specializations at this point, so only the primary template is considered, not the specialization. If we had other primary template overloads, these would also be considered, but we only have one in this puzzle. An example of another primary template could, for instance, be this one for pointers:
template<typename T>void serialize(T*) { std::cout << "pointer\n"; }
We can recognize this as a primary template by the non-empty template, just like we saw in the primary template in the puzzle.
Synthesizing a declaration
Since the primary template is the only one of its kind in this puzzle, we’ve now found the function template for serialize
that should be a candidate for overload resolution. But to be able to proceed, overload resolution needs to have a concrete function signature with actual types, not just a template taking an arbitrary type T
. So next, it does type deduction for serialize
to see what a potential instantiation would look like. This is called synthesizing a declaration. This synthesized declaration is added to the set of candidate functions (together with the non-template serialize
), and regular overload resolution proceeds. Let’s see how type deduction and overload resolution work for each of the two calls to serialize
.
Case 1: Passing an object
First, have a look at the call where we pass an object:
serialize(hello_world);
The hello_world
expression is an lvalue of type std::string
. As we learned in the A Constant Struggle puzzle, T
is deduced to std::string
, and the synthesized declaration has the signature void serialize(std::string&)
. We now have these two candidates for overload resolution:
void serialize(std::string&); // Synthesized from the template (1)void serialize(const std::string&); // The non-template function (3)
Since hello_world
is a non-const, overload resolution prefers the template version of serialize
, which takes a non-const reference.
Now that overload resolution has decided to use the function template, it’s time to find a specialization to use. Should it instantiate and use the primary function template or use the explicit specialization? In this case, that’s a simple choice since we need a specialization with the signature void serialize(std::string&)
but our explicit specialization has the signature void serialize(const std::string&)
. We have to use the primary function template, and an implicit instantiation is made for T = std::string
, resulting in void serialize(std::string&)
. When this instantiation is called, template
is printed.
Case 2: Passing a temporary string
Next, let’s look at the call where we pass a temporary string:
serialize(std::string{"Good bye, world!"});
The std::string{"Good bye, world!"}
expression is a prvalue of type std::string
, so T
is deduced to std::string
, just like for the previous call. We again have these two candidates for overload resolution:
void serialize(std::string&); // Synthesized from the template (1)void serialize(const std::string&); // The non-template function (3)
Since std::string{"Good bye, world!"}
is a prvalue, the non-const reference in the function template can’t bind to it. The const reference in the normal function can, and this function is picked by overload resolution. Since the template was not picked, no instantiation takes place and normal function
is printed.
What would happen if the function template also took a const reference, like this?
template<typename T>
void serialize(const T&) { std::cout << "template\n"; } // 1
Notice that in neither of the preceding cases was the explicit function template specialization even involved in overload resolution—it never is. This can be non-intuitive, so in general, it’s better to use non-template overloads than explicit specializations.
Recommendations
Here are some recommendations for handling function templates and overload resolution:
Prefer non-template overloads over explicit specializations: Non-template overloads can be more flexible and easier to manage than explicit specializations. They integrate seamlessly with template argument deduction and avoid some of the complexities and potential pitfalls of template specializations.
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.