Perfect forwarding and overload resolution

One 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.

11 Responses to “Perfect forwarding and overload resolution”

  1. sap Says:

    nice article.

    is this correct?
    f (s); // f(T&&) [T = std::string&

    tried it on my visual studio 2011 machine and it called the correct one f(string& s) and not the templated one like you say which makes sense to me (i was having a hard time understanding why it would call the templated one).

    thanks for the article great stuff

  2. Boris Kolpackov Says:

    Yes, without the enable_if addition, f(s) will be resolved to the forwarding function. I tested this with GCC 4.7 and now confirmed with Clang 3.0.

    Note also that the non-template function in the post has the void f (const string&) signature, not void f (string&) as in your comment (that would certainly explain the difference in behavior). The other possibility is that VC11 implements v2.1 of the rvalue reference specification while GCC and Clang implement v3, which is what’s in the standard.

  3. sap Says:

    oops the problem was indeed the missing const, not it works as you say.
    great stuff.

  4. Joe Says:

    I am not sure this is a fair criticism of rvalue refs or perfect forwarding.

    I think you would run into the exact same issues if you used a simple rvalue ref

    template
    void f (T & x)
    {
    g (x);
    }

    if instead you use

    template
    void f (T x)
    {
    g (x);
    }

    Then f(s) will use the expected specialization, but f(”bbb”) will use the template.

    The general lesson I’ve learned in the past is that overloading templates and non-templates that vary in cv qualifications or reference is asking for trouble. If a function signature doesn’t match the argument type exactly, it’s likely the template will be chosen.

  5. Andrew Nelless Says:

    You could have also created a template for static char arrays.

    template
    void f (char const (&arr)[N]) { f(std::string((char const*) arr, N)); }

    Doing this will handle all the char array cases and (almost insignificantly) speed up the std::string construction. If your array reference contains binary data (a null char) this would actually be the safer option anyway (I think).

    Personally I don’t feel the binding of f(s) to [T = std::string&] instead of to (const std::string&) is a bad thing either. Your perfect forwarding function could forward to a function capable of receiving a mutable reference. The alternative would be to force binding to const& over T&& and screw you over when you wanted to do just that.

  6. Boris Kolpackov Says:

    Joe, Andrew, I agree there are different ways to resolve this issue. The approach I suggested is general enough that it can be applied to any set of overloaded functions while your approaches could be a better fit for this particular example.

  7. Joe Says:

    I wasn’t trying to resolve the specific example. I was only trying to demonstrate that the same problem in the example exists if you overload with an lvalue ref template and, to a lesser extent, a non-reference template. With the point being that this particular trap is not rvalue ref/perfect forwarding specific.

  8. Andrew Nelless Says:

    Hi Boris, your ‘hairy’ example is missing “::type” on the end.

    I don’t quite agree with your solution. First and foremost your template needs to be updated so it is disabled every time a “const Foo&” overload is added. If you’re doing that, why not just provide a f(Foo&) overload in the first place?

    Convertibility testing is also imperfect. Let’s suppose you’ve only implementing f(int) and a user of your function developers a PrimeNumber class and declares a f(PrimeNumber const&) overload. Let’s also suppose this function is in a separate header file and merely forward declares PrimeNumber. Your template will deal with non-const PrimeNumber& lvalue references but, because the only way to provide implicit convertibility to int is via a conversion operator, will silently fail the is_convertible test because the full definition of the PrimeNumber class isn’t unavailable.

    The result your template is still selected over the const& variant and your user is going to get unexpected linker errors or inconsistent behaviour.

  9. Andrew Nelless Says:

    Actually nevermind, please delete this and the above.

  10. Andrew Nelless Says:

    Double actually, delete this and the above but not the above of the above ;) I’m right on this.

  11. Andrew Nelless Says:

    Here’s some code that demonstrates what I mean:

    http://codepad.org/mx41dQZu