mak February 2016

Why is this call of overloaded function ambiguous?

Why is this constructor call ambiguous?

#include <functional>

class A {
    std::function<int(void)> f_;
    std::function<float(void)> g_;
public:
    A(std::function<int(void)> f) { f_ = f; }
    A(std::function<float(void)> g) { g_ = g; }
    ~A() {}
};

int main()
{
    A a([](){ return (int)1; });
    return 0;
}

Note the typecast.

Is there a way to tell the compiler which constructor overload to use?

Answers


Crazy Eddie February 2016

Because what you are passing doesn't match the types so we enter into conversion sequences to find the overload to use. Both versions of function can be implicitly created from a lambda object that returns int. Thus the compiler can't decide which to choose to create; though it seems intuitively obvious the rules in C++ don't allow for it.

Edit:

Written off the cuff but I think this could do the trick:

template < typename Fun >
typename std::enable_if<std::is_same<typename std::result_of<Fun()>::type, int>::value>::type f(Fun f) ...


template < typename Fun >
typename std::enable_if<std::is_same<typename std::result_of<Fun()>::type, double>::value>::type f(Fun f) ...

etc... Or you might use tag dispatching:

template < typename Fun, typename Tag >
struct caller;

template < typename T > tag {};

template < typename Fun >
struct caller<Fun, tag<int>> { static void call(Fun f) { f(); } };

// etc...

template < typename Fun >
void f(Fun fun) { caller<Fun, typename std::result_of<Fun()>>::call(fun); }


user5900716 February 2016

It's a defect in the standard. See DR 2132:

Consider the following:

#include <functional>

void f(std::function<void()>) {}
void f(std::function<void(int)>) {}

int main() {
  f([]{});
  f([](int){});
}

The calls to f in main are ambiguous. Apparently because the conversion sequences to std::function from the lambdas are identical. The standard specifies that the function object given to std::function "shall be Callable (20.8.11.2) for argument types ArgTypes and return type R." It doesn't say that if this is not the case, the constructor isn't part of the overload set.

Try using a function pointer as an argument instead:

A(int f()) { f_ = f; }
A(float g()) { g_ = g; }


Yakk February 2016

template<class F,
  class R=std::result_of_t<F&()>,
  std::enable_if_t<std::is_convertible<R,int>{}&&(!std::is_convertible<R,float>{}||std::is_integral<R>{}),int> =0
>
A(F&&f):A(std::function<int()>(std::forward<F>(f))){}
template<class F,
  class R=std::result_of_t<F&()>,
  std::enable_if_t<std::is_convertible<R,float>{}&&(!std::is_convertible<R,int>{}||std::is_floating_point<R>{}, int> =0
>
A(F&&f):A(std::function<float()>(std::forward<F>(f))){}
A(std::function<int()> f) { f_ = f; }
A(std::function<float()> g) { g_ = g; }

Here we take ambiguous cases, and dispatch integral ones to the int overload, and non-floating to the float one.

Cases where it is convertible to both, yet neither floating point nor integral, remain ambiguous. As they should.

The sfina clause can be tricky. If it does not work, replace:

  std::enable_if_t<std::is_convertible<R,float>{}&&(!std::is_convertible<R,int>{}||std::is_floating_point<R>{}, int> =0

With

  class=std::enable_if_t<std::is_convertible<R,float>{}&&(!std::is_convertible<R,int>{}||std::is_floating_point<R>{}>

and similar for other ctor. This might work in MSVC for example.

Full on tag dispatching may be required for msvc, due to lack of expression sfinae in 2015.

Post Status

Asked in February 2016
Viewed 3,221 times
Voted 6
Answered 3 times

Search




Leave an answer