std::bind to a std::variant containing multiple std::function types
Asked Answered
F

4

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 {});
Furfuran answered 29/8, 2019 at 11:41 Comment(6)
On a semi-related note: why must noexcept be specified for the lambda in the working example for compilation to pass?Furfuran
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.Currant
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.Flowered
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)).Jodhpurs
@Jonas, Oops, I missed that I had most warnings enabled. Compilation fails because -Werror=noexcept is specified.Furfuran
@MarekR, I use std::bind with lambdas for brevity. In actual code I would only use it here inside the templated register function.Furfuran
T
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.

Twofaced answered 29/8, 2019 at 13:2 Comment(2)
Jason Turner recently discussed his attempts at reimplementing bind_front in episodes 174, 177, and 181 of C++ Weekly. Might be interesting in lieu of C++20 availability.Currant
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.Harbourage
D
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)...); };
}
Dariusdarjeeling answered 29/8, 2019 at 11:57 Comment(0)
T
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.

Twofaced answered 29/8, 2019 at 13:2 Comment(2)
Jason Turner recently discussed his attempts at reimplementing bind_front in episodes 174, 177, and 181 of C++ Weekly. Might be interesting in lieu of C++20 availability.Currant
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.Harbourage
H
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.

Harbourage answered 29/8, 2019 at 12:40 Comment(0)
T
-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.

Torsk answered 29/8, 2019 at 11:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.