Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Overload ambiguity with std::function that takes an std::optional parameter

Tags:

c++

c++17

Here is a simple example program:

using fn_string = function<void(const string&)>;
using fn_optional_string = function<void(const optional<string>&)>;

void foo(fn_string) { cout << "string" << endl; }
void foo(fn_optional_string) { cout << "optional string" << endl; }

int main()
{
    foo([&](const string&){ });
    foo([&](const optional<string>&){ }); // <-- ambiguous
    return 0;
}

It has 2 overloads for foo() -- one taking function with string parameter and another with optional<string>.

Why is 2nd call to foo() ambiguous?

Is there a simple way to fix it? Without casts?

UPDATE

The above was an overly simplified example of the following real world problem I am trying to solve, which is:

using delegate = variant<
    function<void()>,
    function<void(const string&)>,
    function<void(const optional<string>&)>
>;

struct foo
{
    void add_delegate(delegate fn) { fns.push_back(std::move(fn)); }
    vector<delegate> fns;
};

int main()
{
    foo bar;
    bar.add_delegate([&](){ });
    bar.add_delegate([&](const string&){ });
    bar.add_delegate([&](const optional<string>&){ }); // ERROR
    return 0;
}

The last call to add_delegate fails to compile, as it can't decide between function<void(const string&)> and function<void(const optional<string>&)>.

My understanding was that the issue had to do with overload resolution (hence my original example). What change should I make to add_delegate to permit it to accept all 3 versions of lambdas?

Complete example can be found on Coliru.

like image 646
Innocent Bystander Avatar asked Mar 30 '18 02:03

Innocent Bystander


2 Answers

A lambda is not a std::function<>. A std::function<R(Args...)> is a type-erasure value type that can store any copyable object that is call-compatible with R(Args...).

In one case above, R is void (which for a std::function means "I don't care what it returns), and Args... is std::string. A callable object is call-compatible with this if you can call it with a std::string rvalue.

This is true of both std::optional<std::string> and std::string.

There is no special overload for "exact match" -- all that matters is, call compatible or not.

There are a few ways to handle this.

template<std::size_t N>
struct overload_order : overload_order<N-1> {};
template<>
struct overload_order<0> {};

namespace details {
  void foo(overload_order<1>, fn_string) { cout << "string" << endl; }
  void foo(overload_order<0>, fn_optional_string) { cout << "optional string" << endl; }
}
template<class F>
void foo(F&& f) {
  foo( overload_order<!std::is_same<std::decay_t<F>, fn_optional_string>{}>{}, std::forward<F>(f) );
}

now we first try the fn_string one, and only if that fails do we try fn_optional_string, unless the argument is already a fn_optional_string, in which case we dispatch directly to that overload.

like image 106
Yakk - Adam Nevraumont Avatar answered Nov 05 '22 15:11

Yakk - Adam Nevraumont


Declare the argument specifically as a fun_optional_string.

I don't know what to type to keep the software from complaining about a code only answer, so here's a poem:

There is an old hack from Milpitas... His motto, "No bug can defeat us." ... His resolve never lapses ... As he fires those synapses ... Fueled by doughnuts, cold rice, and fajitas. ...

#include <functional>
#include <iostream>
#include <optional>
#include <string>
using namespace std;

using fn_string = function<void(const string&)>;
using fn_optional_string = function<void(const optional<string>&)>;

void foo(fn_string) { cout << "string" << endl; }
void foo(fn_optional_string) { cout << "optional string" << endl; }

int main()
{
    foo([&](const string&){ });
    fn_optional_string g = [&](const optional<string>&) {};
    foo(g); // <-- not ambiguous
    return 0;
}
like image 1
Jive Dadson Avatar answered Nov 05 '22 15:11

Jive Dadson