std::bind 到包含多个 std::function 类型的 std::variant

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

我正在研究回调函数,希望通过 std::bind 注册多个签名不同的函数(尽管它们都是 return void)。将 std::bind 的结果分配给 std::variant 会产生 "conversion to non-scalar type" 错误。这是歧义错误吗?我可以向编译器提供更多信息吗?

删除 std::bind(允许分配)不是一个选项,因为我希望使用一些

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

例如:

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

有效,但是

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

没有,虽然我希望它编译成包含 std::function<void()>.

std::variant

我在 GCC 7.4.0 中使用 -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 {});

std::bind returns 满足特定要求但不允许根据签名区分函数类型的未指定对象。初始化

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

简直就是模棱两可,和

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

您可以明确说明要实例化的类型,例如

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

这需要一些类型别名,但基本上可以。更好的选择可能是避免使用 std::bind 而是使用 lambda。示例:

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

原因是 std::bindstd::function 产生不同的类型(它是一个未指定/可调用类型)- 所以虽然可转换 - 但它是不明确的。

std::bind 的一个特点是它对 额外的 参数的处理。考虑:

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

bound_f() 调用 f(42) 得到 43。但 bound_f("hello")bound_f(2.0, '3', std::vector{4, 5, 6}) 给你 43 的情况。调用站点上没有关联占位符的所有参数都将被忽略。

这里的意义在于is_invocable<decltype(bound_f), Args...>对所有类型集合都成立Args...


回到你的例子:

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

右边的绑定和前面的bound_f很像。它可以用 any 参数集调用。它可以不带参数调用(即可以转换为 std::function<void()>)并且可以使用 int 调用(即可以转换为 std::function<void(int)>)。也就是说,变体的两种选择都可以从 bind 表达式构造,我们无法将其中一个与另一个区分开来。他们都只是转换。因此,模棱两可。

我们 不会 有 lambda 的这个问题:

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

这很好用,因为 lambda 只能在没有参数的情况下调用,所以只有一种选择是可行的。 Lambda 不只是丢弃未使用的参数。

这概括为:

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);
        });
}

虽然如果你真的想在这里传递占位符,这是行不通的。这真的取决于你更大的设计,这里的正确解决方案可能是什么。

您可以使用 c++20 std::bind_front 它将编译:

#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

根据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.