twentylemon
twentylemon

Reputation: 1246

std::function overloads have similar conversions

I'm in the process of writing up an STL-like library for learning purposes. All of the collections extend a class called Iterable which contains wrapping functions for all of the functions found in <algorithm>. For example, it allows vec.each([](T t){...}); which I strongly prefer over the verbose std::for_each. The function giving me problems is count - I want to overload Iterable::count so it combines the behaviour of both std::count and std::count_if depending on the argument type but I'm running into a strange error.

Iterable.h

virtual int count(const T& value) const {
    return std::count(begin(), end(), value);
}
virtual int count(std::function<bool(T&)> predicate) {
    return std::count_if(begin(), end(), predicate);
}
virtual int count(std::function<bool(const T&)> predicate) const {
    return std::count_if(begin(), end(), predicate);
}

main.cpp

Vector<int> vec; // extends Iterable
vec.add(0);
vec.add(1);
vec.count([](int i){ return i == 0; }); // compiles and works fine
vec.count(0); // error c2666: 3 overloads have similar conversions

I should note that changing the count_if wrapper function names to count_if does work and resolves the ambiguity, but I'd prefer to have them named count and also to figure out why there is ambiguity in the first place.

From what I interpret, the compiler is trying to make a new std::function using the template <class F> function(F f) ctor, then runs into the ambiguity. Is that the case? It seems odd since the line below fails to compile as well.

std::function<bool(int)> f(0); // error C2064: term does not evaluate to a function taking 1 arguments

Any insights or potential fixes are much appreciated.

Forgot to say; using visual studio 2012, nov 2012 ctp compiler

Upvotes: 3

Views: 662

Answers (2)

tux3
tux3

Reputation: 7330

The problem is that 0 is ambiguous here, it can be interpreted as a null pointer or an int, which makes it match both the std::function constructor and the more general const T& value (both require a conversion).

If you don't want to change the interface, you can just create a very simple function template to deduce and dispatch the arguments.

C++11 version:

template<typename U>
int count(U&& value) const {
    return count_impl(std::forward<U>(value));
}

This works because the function template type deduction rules don't have that ambiguity, they never treat 0 as a null pointer.

So your interface is now:

virtual int count_impl(const T& value) const {
    return std::count(v.begin(), v.end(), value);
}
virtual int count_impl(std::function<bool(T&)> predicate) {
    return std::count_if(v.begin(), v.end(), predicate);
}
virtual int count_impl(std::function<bool(const T&)> predicate) const {
    return std::count_if(v.begin(), v.end(), predicate);
}

template<typename U>
int count(U&& value) const {
    return count_impl(std::forward<U>(value));
}

And you can use it naturally:

int main(){
    Vector<int> vec; // extends Iterable
    vec.count([](int i){ return i == 0; }); // compiles and works fine
    vec.count(0); // no problem, calls virtual int count_impl(const T& value) const
}

Upvotes: 3

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275820

std::function<Sig> in the published C++11 standard without errata contains a constructor that thinks it can consume anything, as far as its signature is concerned. If you pass it things it cannot consume (things that are not callable), it fails to compile.

Overload resolution occurs earlier (based on shallower information) than the compile failure. It matches on signatures, not implementations.

A bug report and a fix was proposed, so some C++11 compilers can fix this, and all C++14 compilers must fix this.

VS2012 has limited SFINAE overload resolution capabilities. But one approach would look like:

template<class Sig, class=void>
struct is_filter_on : std::false_type{};
template<class F, class Arg>
struct is_filter_on< F(Arg),
  typename std::enable_if<std::is_convertible<
    typename std::result_of<F(Arg)>::type
  ,bool>::value>::type
> : std::true_type{};

which is an attempt at a traits class that tells you if F(Arg) is a bool-returning "filter" on values of type Arg.

template<class X>
size_t count(X&& x) const {
  return count( std::forward<X>(x), is_filter_on< X&(T const&) >{} );
}
template<class X>
size_t count(X&& x) {
  return count( std::forward<X>(x), is_filter_on< X&(T&) >{} );
}

template<class F>
size_t count(F&& f, std::true_type) const {
  return std::count_if( begin(), end(), std::forward<F>(f) );
}
template<class F>
size_t count(F&& f, std::true_type) {
  return std::count_if( begin(), end(), std::forward<F>(f) );
}
template<class X>
size_t count(X&& x, std::false_type) const {
  return std::count( begin(), end(), std::forward<X>(x) );
}
template<class X>
size_t count(X&& x, std::false_type) {
  return std::count( begin(), end(), std::forward<X>(x) );
}

but I have no idea of MSVC2012 will work with the above.

Here I use tag dispatching to pick which version of count I call. The traits class is_filter_on does a test to determine if the pseudo-expression F(Arg) is filter-like. If so, we dispatch to the std::count_if. Otherwise, we dispatch to the std::count version.

Upvotes: 4

Related Questions