std::bind to a std::variant containing multiple std::function types

std::bind returns an unspecified object that satisfies certain requirements, but doesn't allow for a distinction between function types based on a signature. The initialization

std::variant<std::function<void()>, std::function<void(int)>> v =
    std::bind([]() noexcept {});

is simply ambiguous, same as

std::variant<int, int> v = 42; // Error, don't know which one

You can be explicit about the type you intend to instantiate, e.g.

std::variant<std::function<void()>, std::function<void(int)>> v =
    std::function<void()>{std::bind([]() noexcept {})};

This cries for some type aliases, but basically works. A better alternative might be to avoid std::bind and instead use lambdas, too. Example:

template <typename Function, typename... Args>
void registerFunc(Function &&f, Args &&... args)
{
    variant_of_multiple_func_types =
       [&](){ std::forward<Function>(f)(std::forward<Args>(args)...); };
}

You can using c++20 std::bind_front and it will compile:

#include <functional>
#include <variant>

int main()
{
    std::variant<std::function<void()>, std::function<void(int)>> v = std::bind_front([]() noexcept {});
    std::get<std::function<void()>>(v)();
}

Live demo

According to cppreference:

This function is intended to replace std::bind. Unlike std::bind, it does not support arbitrary argument rearrangement and has no special treatment for nested bind-expressions or std::reference_wrappers. On the other hand, it pays attention to the value category of the call wrapper object and propagates exception specification of the underlying call operator.


One of the features of the std::bind is what it does with extra arguments. Consider:

int f(int i) { return i + 1; }
auto bound_f = std::bind(f, 42);

bound_f() invokes f(42) which gives 43. But it is also the case that bound_f("hello") and bound_f(2.0, '3', std::vector{4, 5, 6}) gives you 43. All arguments on the call site that don't have an associated placeholder are ignored.

The significance here is that is_invocable<decltype(bound_f), Args...> is true for all sets of types Args...


Getting back to your example:

std::variant<std::function<void()>, std::function<void(int)>> v =
    std::bind([]() noexcept {});

The bind on the right works a lot like bound_f earlier. It's invocable with any set of arguments. It is invocable with no arguments (i.e. it is convertible to std::function<void()>) and it is invocable with an int (i.e. it is convertible to std::function<void(int)>). That is, both alternatives of the variant can be constructed from the bind expression, and we have no way of distinguishing one from the other. They're both just conversions. Hence, ambiguous.

We would not have this problem with lambdas:

std::variant<std::function<void()>, std::function<void(int)>> v =
    []() noexcept {};

This works fine, because that lambda is only invocable with no arguments, so only one alternative is viable. Lambdas don't just drop unused arguments.

This generalizes to:

template <typename Function, typename... Args>
void register(Function &&f, Args &&... args)
{
    variant_of_multiple_func_types =
        [f=std::forward<Function>(f), args=std::make_tuple(std::forward<Args>(args)...)]{
            return std::apply(f, args);
        });
}

Though if you want to actually pass placeholders here, this won't work. It really depends on your larger design what the right solution here might be.