Perfect forwarding and overload resolution
Wednesday, May 30th, 2012One of the things that are made possible in C++11 thanks to rvalue references is perfect forwarding. I am sure you’ve already read about this feature and seen some examples. However, as with many new features in C++11, when we move beyond toy code and start using them in real applications, we often discover interactions with other language features that weren’t apparent initially. Interestingly, I’ve already run into a somewhat surprising side effect of perfect forwarding, which I described in the “Rvalue reference pitfalls” post. Today I would like to explore another such interaction, this time between perfect forwarding and overload resolution.
If we have just have one function (or constructor, etc.) that implements perfect forwarding, then the compiler’s job in resolving a call to such a function is straightforward. However, if a forwarding function is part of an overloaded set, then things can get interesting. Suppose we have the following existing set of overloaded functions:
void f (int); void f (const std::string&);
And here are some sample calls of these functions:
int i = 1; std::string s = "aaa"; f (i); // f(int) f (2); // f(int) f (s); // f(string) f ("bbb"); // f(string)
Suppose we now want to add a “fallback” function that forwards the call to some other function g()
. The idea here is that our fallback function should only be used if none of the existing functions can be called. Sounds easy, right?
template <typename T> void f (T&& x) { g (x); }
With this addition, let’s see which functions get called for the above example:
f (i); // f(int) f (2); // f(int) f (s); // f(T&&) [T = std::string&] f ("bbb"); // f(T&&) [T = const char (&)[4]]
Whoa, what just happen? This is definitely not something that we want. We expect the C++ compiler to select the most specialized functions which in this case means the compiler should prefer non-templates over our forwarding function. As it turns out, this is still the rule but things get a bit more complex because of the perfect forwarding. Remember that the whole idea of perfect forwarding is that we get the perfect parameter type for whatever argument we pass. The C++ compiler will still choose the most specialized function (non-template in our case) over the forwarding function if it has the parameter type that matches the argument perfectly. Otherwise the forwarding function is a better match.
With this understanding it is easy to explain the above example. In the first call the argument type is int&
. Both f(int)
and f(int&)
(signature of the forwarding function after reference collapsing) match perfectly and the former is selected as the most specialized. The same logic applies to the second call except that the signature of the forwarding function becomes f(int&&)
.
The last two calls are more interesting. The argument type of the second last call is std::string&
. Possible matches are f(const std::string&)
and f(std::string&)
(again, signature of the forwarding function after reference collapsing). In this case, the second signature is a better match.
In the last call, the argument type is const char (&)[4]
(reference to an array of four constant characters). While the forwarding function is happy to oblige again, f(const std::string&)
can only be applied with implicit conversion. So it is not even considered! Note something else interesting here: it is perfectly plausible that the call to g()
inside forwarding f()
will also require an implicit conversion. But that fact is not taken into account during overload resolution of the call to f()
. In other words, perfect forwarding can hide implicit conversions.
Ok, now we know why our code doesn’t work. The question is how do we fix it? From experience (see the link above), it seems that the best way to resolve such issues is to disable the forwarding function for certain argument types using std::enable_if
. Ideally, the test that we would like to perform in plain English would sound like this: “can any of the non-forwarding functions be called with this argument type”? If the answer is yes, then we disable the forwarding function. Here is the outline:
template <typename T, typename std::enable_if<!test<T>::value, int>::type = 0> void f (T&& x) { ... }
Unfortunately, there doesn’t seem to be a way to create such a test (i.e., there is no way to exclude the forwarding function from the overload resolution; though if you know how to achieve something like this, do tell). The next best thing is to test whether the argument type is the same as or can be implicitly converted to the parameter type of a non-forwarding function. Here is how we can fix our code using this method:
template <typename T, typename std::enable_if< !(std::is_same<T, std::string&>::value || std::is_convertible<T, std::string>::value), int>::type = 0> void f (T&& x) { ... }
Besides looking rather hairy, the obvious disadvantage of this approach is that we have to do this test for every non-forwarding function. To make the whole thing tidier we can create a helper that allows us to test multiple types at once:
template <typename T, typename disable_forward<T, std::string, std::wstring>::type = 0> void f (T&& x) { ... }
Here is the implementation of disable_forward
using C++11 variadic templates:
template <typename T, typename T1, typename ... R> struct same_or_convertible { static const bool value = std::is_same<T, T1>::value || std::is_convertible<T, T1>::value || same_or_convertible<T, R...>::value; }; template <typename T, typename T1> struct same_or_convertible<T, T1> { static const bool value = std::is_same<T, T1>::value || std::is_convertible<T, T1>::value; }; template <typename T, typename ... R> struct disable_forward { static const bool value = same_or_convertible< typename std::remove_reference< typename std::remove_cv<T>::type>::type, R...>::value; typedef typename std::enable_if<value, int> type; };
And the overall moral of the story is this: when using perfect forwarding watch out for unexpected interactions with other language features.