10

I'm playing around with callback functions and wish to register multiple functions via std::bind that differ in signatures (altough they all return void). Assigning the result of the std::bind to the std::variant yields in a "conversion to non-scalar type" error. Is it an ambiguity error? Can I provide the compiler with more information?

Dropping the std::bind (which allows the assignment) is not an option as I wish to register the callbacks using some

template <typename Function, typename... Args>
void register(Function &&f, Args &&... args)
{
    variant_of_multiple_func_types = std::bind(f, args...);
}

For example:

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

works, but

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

does not, while I expect it to compile into a std::variant containing a std::function<void()>.

I get the following compilation error in GCC 7.4.0 with -std=c++17:

error: conversion from ‘std::_Bind_helper<false, main(int, char**)::<lambda()> >::type {aka std::_Bind<main(int, char**)::<lambda()>()>}’ to non-scalar type ‘std::variant<std::function<void()>, std::function<void(int)> >’ requested
     std::variant<std::function<void()>, std::function<void(int)>> v = std::bind([]() noexcept {});
Tmplt
  • 127
  • 1
  • 7
  • On a semi-related note: why must `noexcept` be specified for the lambda in the working example for compilation to pass? – Tmplt Aug 29 '19 at 11:43
  • Could you explain what you by "why must `noexcept` be specified for the lambda in the working example"?. Omitting the `noexcept` still works for me using the same compiler and flags. – Jonas Greitemann Aug 29 '19 at 11:49
  • 4
    why are you using `std::bind` with lambda? `std::bind` was useful when lambda could not be used. In your code `std::bind` is just obsolete. – Marek R Aug 29 '19 at 11:57
  • `std::bind([]() noexcept {})("lot", "of", "unused", "parameter")` is valid. you can pass any number of parameter to the bind object, it is mostly needed for the `place_holder` (`std::bind([](int) {/*..*/}, place_holder::_5)`). – Jarod42 Aug 29 '19 at 12:31
  • @Jonas, Oops, I missed that I had most warnings enabled. Compilation fails because `-Werror=noexcept` is specified. – Tmplt Aug 29 '19 at 13:07
  • @MarekR, I use `std::bind` with lambdas for brevity. In actual code I would only use it here inside the templated `register` function. – Tmplt Aug 29 '19 at 13:08

4 Answers4

9

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)...); };
}
lubgr
  • 37,368
  • 3
  • 66
  • 117
7

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.

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • 1
    Jason Turner recently discussed his attempts at reimplementing `bind_front` in episodes [174](https://www.youtube.com/watch?v=7pqPfQ_HpmI), [177](https://www.youtube.com/watch?v=fLeHy7s1WIo), and [181](https://www.youtube.com/watch?v=s2Kqcn5e73c) of _C++ Weekly_. Might be interesting in lieu of C++20 availability. – Jonas Greitemann Aug 29 '19 at 13:14
  • It's weird to use `bind_front` in a situation where we're intentionally not taking any extra arguments. We're using `bind_front(f)` where `f` would work fine. – Barry Aug 29 '19 at 13:31
4

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.

Tmplt
  • 127
  • 1
  • 7
Barry
  • 286,269
  • 29
  • 621
  • 977
-1

The reason is that the std::bind results in a different type (it's an unspecified / Callable type) than std::function- so while convertible - it's ambiguous.

darune
  • 10,480
  • 2
  • 24
  • 62