Overload resolution with universal references
Asked Answered
A

2

9

I have a function which can accept any type by universal reference, and would like to overload it for specific types (some of which are templated themselves, although I don't think that's important here). Unfortunately I can't quite seem to to get the overloads to be resolved in the right order.

I would have presumed that the second declaration of foo would be preferred as it's more specific (less templated), although it looks like my understanding of overload resolution is lacking somewhat. Interestingly changing the second declaration to take X by value makes it print "good, good" and making it take X by non-const reference makes it print "bad, good". Obviously removing the first declaration entirely makes it return "good, good", as there's no other choice.

So why does this happen? And most importantly, if the following code doesn't work, how can you overload a function with this signature?

#include <iostream>
#include <string>

class X {};

template<typename T>
inline std::string foo(T && rhs) {
    return "bad";
}

inline std::string foo(const X & rhs) {
    return "good";
}

int main() {
    std::cout << foo(X()) << std::endl;
    X x;
    std::cout << foo(x) << std::endl;
    return 0;
}

Edit:

Maybe a more roundabout solution to this is to do it indirectly. Get rid of the first form of foo and use SFINAE to check if a valid overload exists, it it doesn't then call foo_fallback.

Akkad answered 22/5, 2013 at 14:51 Comment(2)
I've come up with a solution to the overloading part: mortoray.com/2013/06/03/…Alcine
1+ hour youtube video on this exact problem, which amusingly links back to this question.Probative
O
4

To answer your question.comment to Kerre's answer, you could try to use SFINAE:

#include <type_traits>
#include <string>

template <class T>
struct HasFooImpl_ {
  template <typename C>
  static std::true_type test(decltype(fooImpl(std::declval<C>()))*);
  template <typename C> 
  static std::false_type test(...);
  typedef decltype(test<T>(0)) type;
};

template <typename T>
using HasFooImpl = typename HasFooImpl_<T>::type;

template <typename T>
typename std::enable_if<HasFooImpl<T>::value, std::string>::type 
foo(T&& t)
{
  return fooImpl(std::forward<T>(t));
}

template <typename T>
typename std::enable_if<!HasFooImpl<T>::value, std::string>::type
foo(T&& t)
{
    return "generic!";
}

You'd have to implement a function fooImpl for any type that you don't want to be handled genericly.

The implementation was a bit tricky, I tried just enable_if<is_same<string, decltype(fooImpl(declval<C>()))>::value first, but for the fallback the !is_same<>::value gave me compiler errors, because it tried to instantiate the decltype as well.

This implementation has one caveat that you might or might not want to use: if T is convertible to some other type that has a fooImpl defined, that conversion will kick in.

You can see the whole thing in action here: http://ideone.com/3Tjtvj

Update: if you don't want to allow type conversions, it actually gets easier:

#include <type_traits>
#include <string>

template <typename T> void fooImpl(T);

template <typename T>
using HasFooImpl = typename std::is_same<std::string, decltype(fooImpl(std::declval<T>()))>;

template <typename T>
typename std::enable_if<HasFooImpl<T>::value, std::string>::type 
foo(T&& t)
{
  return fooImpl(std::forward<T>(t));
}

template <typename T>
typename std::enable_if<!HasFooImpl<T>::value, std::string>::type
foo(T&& t)
{
    return "generic!";
}

See http://ideone.com/miaoop

Ovular answered 22/5, 2013 at 16:2 Comment(2)
I like this. Here's another implementation of the same idea, but using expression SFINAE instead of a helper class and enable_if (the downside is that it requires another level of indirection).Akkad
I actually prefer the latter's conversions semantics, and the implementation's very neat. Excellent.Akkad
F
4

The conversion from X to const X is considered worse than the direct match of the templated overload with T = X or T = X &.

Fidellia answered 22/5, 2013 at 14:53 Comment(5)
So how do you make the universal reference version take a backseat to more specific versions?Akkad
@jleahy: You could make three non-templated overloads for X &, X const & and X && to cover all cases... and yes :-) Edit: see here.Fidellia
@jleahy: Or, if you only want to exclude X, use enable_if in the template (and decay).Fidellia
@Akkad I assume from the question that you have more types than only X, right? I am working on an answer for those cases. Stay tuned ;)Ovular
@Arne Mertz Yes, the idea was that X&& would be a generic fallback.Akkad
O
4

To answer your question.comment to Kerre's answer, you could try to use SFINAE:

#include <type_traits>
#include <string>

template <class T>
struct HasFooImpl_ {
  template <typename C>
  static std::true_type test(decltype(fooImpl(std::declval<C>()))*);
  template <typename C> 
  static std::false_type test(...);
  typedef decltype(test<T>(0)) type;
};

template <typename T>
using HasFooImpl = typename HasFooImpl_<T>::type;

template <typename T>
typename std::enable_if<HasFooImpl<T>::value, std::string>::type 
foo(T&& t)
{
  return fooImpl(std::forward<T>(t));
}

template <typename T>
typename std::enable_if<!HasFooImpl<T>::value, std::string>::type
foo(T&& t)
{
    return "generic!";
}

You'd have to implement a function fooImpl for any type that you don't want to be handled genericly.

The implementation was a bit tricky, I tried just enable_if<is_same<string, decltype(fooImpl(declval<C>()))>::value first, but for the fallback the !is_same<>::value gave me compiler errors, because it tried to instantiate the decltype as well.

This implementation has one caveat that you might or might not want to use: if T is convertible to some other type that has a fooImpl defined, that conversion will kick in.

You can see the whole thing in action here: http://ideone.com/3Tjtvj

Update: if you don't want to allow type conversions, it actually gets easier:

#include <type_traits>
#include <string>

template <typename T> void fooImpl(T);

template <typename T>
using HasFooImpl = typename std::is_same<std::string, decltype(fooImpl(std::declval<T>()))>;

template <typename T>
typename std::enable_if<HasFooImpl<T>::value, std::string>::type 
foo(T&& t)
{
  return fooImpl(std::forward<T>(t));
}

template <typename T>
typename std::enable_if<!HasFooImpl<T>::value, std::string>::type
foo(T&& t)
{
    return "generic!";
}

See http://ideone.com/miaoop

Ovular answered 22/5, 2013 at 16:2 Comment(2)
I like this. Here's another implementation of the same idea, but using expression SFINAE instead of a helper class and enable_if (the downside is that it requires another level of indirection).Akkad
I actually prefer the latter's conversions semantics, and the implementation's very neat. Excellent.Akkad

© 2022 - 2024 — McMap. All rights reserved.