C++11 introduced a perfect forwarding mechanism for template parameters. The key part is the ability for a template parameter to match any input without any implicit conversion. This is done using the ‘&&’ operator and has become known as the universal reference. While it does work, it suffers from one serious flaw: it can’t be specialized/overloaded. In this article I present a solution to that problem.
Basic Form
The universal reference is typically used in a template function which forwards its parameters to another function. A very basic function looks like this:
[sourcecode language=”cpp”]
template<typename T>
void apply( T&& value ) {
std::forward<T>( value );
}
[/sourcecode]
A real function would do something more than just forward the value, but here we’re interested just in the universal reference mechanism. The ‘T&&’ part is the key: it says this function should match any input parameter. It is complemented with ‘std::forward’ which retains the exact type when calling the other function. There are several detailed explanations of this part online (refer Scott Meyers), so I’d rather move on to the problem bit here.
The Problem
The problem arises when trying to overload a universal reference for a specific type. Here I mean an unqualified type: one without any lvalue, rvalue, or other modifiers. For example, we wish to write this overload:
[sourcecode language=”cpp”]
void apply( my_type&& value ) {
…
}
[/sourcecode]
We want this function to match ‘my_type’, ‘my_type &’, ‘my_type &&’ and any const modified forms. What we’ve unfortunately written is a function which only matches an rvalue reference. This gets really confusing, so I’ve written a short example to show the problem.
[sourcecode language=”cpp”]
#include <iostream>
template<typename T>
void apply( T&& value ) {
std::cout << "apply" << std::endl;
}
struct my_type { };
void apply( my_type && value ) {
std::cout << "my_type" << std::endl;
}
int main() {
apply( my_type() );
my_type a;
apply( a );
apply( static_cast<my_type const>(a) );
apply( static_cast<my_type const &>(a) );
}
[/sourcecode]
The overloaded ‘my_type’ version is only called one time: the first function call in ‘main’. The three other calls are all to the generic version. The problem is a consistency defect in the C++ standard. While a template ‘T&&’ is a universal reference that matches anything, ‘my_type&&’ is strictly an rvalue reference.
There is no proper way to write the overload we intend. Attempts to specialize the template instead will get the same result: only one function call will match. Continue with the overload method and we’ll be forced to write multiple versions of the function.
[sourcecode language=”cpp”]
void apply(my_type const & value ) {
std::cout << "my_type const&" << std::endl;
}
void apply(my_type & value ) {
std::cout << "my_type const&" << std::endl;
}
[/sourcecode]
If you make this change you’ll see that one of the calls is still to the generic version. By trying to add one which covers the last call I end up getting ambiguity problems. Switching to templates I can get a different set called, but still not a full fix. We could play all day with this nonsense and never get the intended solution.
An Avoidance Solution
Universal references and forwarding are great in some cases, but they simply aren’t needed everywhere. If you don’t have a specific need to capture rvalue references you might just forgo this approach all together. For example, if our ‘apply’ function above needs only to read from the value we can simply use a const reference instead of the universal one. This is the pre-C++11 approach and it still works in many cases.
[sourcecode language=”cpp”]
#include <iostream>
template<typename T>
void apply(T const & value ) {
std::cout << "apply" << std::endl;
}
struct my_type { };
void apply(my_type const & value ) {
std::cout << "my_type" << std::endl;
}
int main() {
apply( my_type() );
my_type a;
apply( a );
apply( static_cast<my_type const>(a) );
apply( static_cast<my_type const &>(a) );
}
[/sourcecode]
This version of the code now calls our overloaded ‘my_type’ version in all cases.
A Universal Solution
A const-reference of course does you no good if you actually need the universal reference. To do this, without duplicating several versions of your overload, you need to resort to a bit of template trickery. I at first attempted to get this working with variations on ‘enable_if’. I was only able to get a single overload working; as I added more I got redefinition errors. So I resorted to a different trickery.
The only way to actually get a universal reference parameter is to use a template. This means all of our overloads must be template functions. The same underlying reason also means we can’t specialize the templates: we need to use actual overloads. And there is no way to avoid redefinition errors with just one parameter, thus we need at least one extra parameter to overload. It would of course be very inconvenient if the caller had to know anything about this.
The solution involves introducing a tag parameter. First, look at the generic ‘apply’ function using this approach.
[sourcecode language=”cpp”]
template<typename T>
struct class_tag { };
template<typename TF>
void apply( TF && f ) {
//get the unqualified type for the purpose of tagging
typename class_tag<typename std::decay<TF>::type> tag;
apply_impl( std::forward<TF>(f), tag );
}
template<typename TF, typename Tag>
void apply_impl( TF && f, Tag ) {
std::cout << f << std::endl;
}
[/sourcecode]
I’ve split the function into two parts now: the generic ‘apply’ call and the ‘apply_impl’ which actually does the processing. This separation exists so that the ‘apply’ function can be called without any knowledge of the underlying tag system.
The ‘class_tag’ is used as an overload helper. Our generic ‘apply_impl’ will match any tag parameter, serving as the fallback for all non-specialized types. The trick is how ‘apply’ adds this extra parameter to the function call. It makes use of the ‘std::decay’ feature: this gives us an unqualified form of the template type. For example, ‘std::decay<match_a const &>’ results in ‘match_a’.
The unqualified type is then used to instantiate a ‘class_tag’ object. This means that every call to ‘apply_impl’ will be done with a tag type which is unique for each unqualified type. Unique types mean we can use them to overload a function without ambiguity. Thus if we wish to overload a ‘match_a’ type we do this:
[sourcecode language=”cpp”]
template<typename TF>
void apply_impl( TF && f, class_tag<match_a> ) {
std::cout << "match_a" << std::endl;
}
[/sourcecode]
This function now applies to the family of ‘match_a’ types: lvalue, rvalue, const, value, etc. We’ve successfully overloaded a universal reference. Here is a full code example to show it all working together.
[sourcecode language=”cpp”]
#include <iostream>
#include <type_traits>
template<typename T>
struct class_tag { };
template<typename TF>
void apply( TF && f ) {
//get the unqualified type for the purpose of tagging
class_tag<typename std::decay<TF>::type> tag;
apply_impl( std::forward<TF>(f), tag );
}
template<typename TF, typename Tag>
void apply_impl( TF && f, Tag ) {
std::cout << f << std::endl;
}
struct match_a { };
template<typename TF>
void apply_impl( TF && f, class_tag<match_a> ) {
std::cout << "match_a" << std::endl;
}
struct match_b { };
template<typename TF>
void apply_impl( TF && f, class_tag<match_b> ) {
std::cout << "match_b" << std::endl;
}
template<typename TF>
void apply_impl( TF && f, class_tag<int*> ) {
std::cout << "int*" << std::endl;
}
int main() {
apply( 12 );
apply( "hello" );
apply( match_a() );
apply( match_b() );
match_a a;
apply(a);
apply( static_cast<match_a const&>(a) );
apply( static_cast<match_a const>(a) );
int b[5];
apply(b);
apply(static_cast<int*>(b));
}
[/sourcecode]
Commentary
I’ve seen some discussions about whether the “universal reference” actually exists. Technically ‘T&&’ is rvalue reference notation that when combined with type deduction rules gives it the universal behaviour. There is no ‘universal reference’ in the standard, though it clearly intended for this functionality. The problem is that ‘T&&’ in a template and ‘my_type&&’ in a normal function behave quite differently, and they serve very different purposes in typical code.
While the universal reference and perfect forwarding is a much welcomed feature in C++, the implementation presented is rather underwhelming. The ‘&&’ notation should not have been given two different meanings. Trying to explain away the issue with type deduction rules is just nonsense. What matters is the result, and the result is that I have to resort to trickery to get a basic overload working. I shouldn’t have to do that, thus I consider it a defect in the C++ language.