从可调用类型中提取签名

Extract signature from callable type

我有一个 std::variant<char, int, double> 类型的值。事实上,我有几个值存储在 std::vector<value>.

我希望能够将这些值传递给多个 callback 函数,前提是它们的签名与值的类型匹配。这是我的实现:

using value = std::variant<char, int, double>;
using values = std::vector<value>;

template<typename... Args>
using callable = std::function<void (Args...)>;

struct callback
{
    template<typename... Args>
    callback(callable<Args...> fn)
    {
        constexpr auto N = sizeof...(Args);
        fn_ = adapt(std::move(fn), std::make_index_sequence<N>());
    }
    
    void operator()(const values& vv) const { fn_(vv); }
    
private:
    std::function<void (const values&)> fn_;

    template<typename... Args, std::size_t... Is>
    auto adapt(callable<Args...> fn, std::index_sequence<Is...>)
    {
        return [fn_ = std::move(fn)](const values& vv) {
            /* if signature matches */ fn_(std::get<Args>(vv[Is])...);
        };
    }
};

using callbacks = std::vector<callback>;

现在我可以做到了,而且效果很好:

void fn(char c, int i) { std::cout << "fn:" << c << i << '\n'; }

int main()
{
    values vv = { 'x', 42 };
    
    callback cb1{ std::function<void (char, int)>(fn) };
    callback cb2{ std::function<void (char, int)>([](char c, int i) { std::cout << "lambda:" << c << i << '\n'; }) };

    callbacks cbs = { cb1, cb2 };
    for(auto const& cb : cbs) cb(vv);

    return 0;
}

你可以看到工作示例here

我已经回顾了几个关于将函数指针、lambda、仿函数等隐式转换为 std::function 的(不)可能性的类似问题。

所以我的问题是,我需要什么“胶水”才能做到这一点:

callback cb3 { fn };
callback cb4 { [](char c, int i) { } };
// same for other callable types

我想到了某种 function_traits 实现来从可调用类型中提取签名并将其传递给 std::function。但是查看 std::is_function 的代码让我觉得这需要大量样板代码。

在这里想请问有没有人能想出更简洁的解决方案。提前谢谢你。


更新

我用 汗水、血和泪水 部分专业化、SFINAE 和 void_t trick 完成了这项工作。我现在可以这样做了:

callback cb1 { fn };
callback cb2 { lambda };
callback cb3 { [](char c, int i) { std::cout << "lambda2:" << c << i << '\n'; } };

int x = 69;
callback cb4 { [x](char c, int i) { std::cout << "lambda3[x]:" << x << c << i << '\n'; } };
callback cb5 { ftor(x) };

这里是demo。在此实现中 callback 可以接受函数指针、lambda(包括捕获,但不是泛型)和函子。

至少对于您描述的用例,您可以通过添加额外的 callback 构造函数模板来实现您想要的。

template<typename... Args>
callback(void (*fn)(Args...)) : callback(callable<Args...>(fn)) {}   

此构造函数将匹配一个可以衰减为函数指针的参数,例如此构造函数调用现在有效

callback cb3{ fn }; // ok

请注意,这个新的构造函数只是通过将函数指针显式转换为 callback<Args...>.

来委托给您的原始构造函数

同样,您可以编写一个模板来匹配 lambda 参数。

template<typename Lambda>
callback(Lambda fn) : callback(+fn) {}

现在这个构造函数调用有效了。

callback cb4{ [](char c, int i) { std::cout << "lambda:" << c << i << '\n'; } }; // ok

这里注意,委托是给构造函数获取一个函数指针。这是通过使用一元 + 运算符将 lambda 显式衰减为函数指针来实现的。当然,该构造函数将进一步委托给您的原始构造函数进行实际实现。


这里是 demo

正如 d_kog 评论的那样,问题本身在 C++ 中很少是正确的。可调用对象不需要有唯一的签名。因此,您应该重新定义问题。不要尝试确定签名来检查调用能力,而是直接检查调用能力。

您的回调必须使用值向量执行三项基本操作:

  1. 将大小“提升”到编译时,例如将向量转换为元组或包。
  2. 访问值,获取变体中的值。
  3. 如果可能,将函数应用于它们。

第 2 步和第 3 步很简单。 “查找签名”用于查找元数,以便索引序列和 vv[Is]... 为您提供一个包(假设首先检查 vv 的 size())。然后你使用 get<Args> 本质上是为了实现 visit,但这是不必要的。

但是步骤1还有其他选项。例如,如果你只支持一定数量的参数,这是有效的:

template<typename F>
void call_helper(const values& vv, F&& f)
{
    switch (vv.size()) {
    case 0: std::forward<F>(f)(); break;
    case 1: std::forward<F>(f)(vv[0]); break;
    case 2: std::forward<F>(f)(vv[0], vv[1]); break;
    case 3: std::forward<F>(f)(vv[0], vv[1], vv[2]); break;
    default: throw std::out_of_range{"too many arguments to callback"};
    }
}

然后adapt变成:

return [fn_ = std::move(fn)](const values& vv) {
    // lift from the vector to function arguments
    call_helper(vv, [&](const auto&... args) {
        // visit the arguments
        std::visit([&](const auto&... args_unwrapped) {
            // check invocability with the arguments
            if constexpr (std::is_invocable_v<decltype(fn_), decltype(args_unwrapped)...>)
                // invoke
                std::invoke(fn_, args_unwrapped...);
            else
                throw std::runtime_error{"invalid argument types to callback"};
        }, args...);
    });
};

然后,您可以支持所有方式的 C++ 可调用对象:

callback cb1{ fn }; // function pointer
callback cb2{ [](char c, int i) { std::cout << "lambda:" << c << i << '\n'; } };
callback cb3{ [](auto c, auto i) { std::cout << "generic lambda:" << c << i << '\n'; }};
int j = 1000;
callback cb4{ [j](char c, int i) { std::cout << "capturing lambda[" << j << "]:" << c << i << '\n'; }};
callback cb5{ [](int c, int i) { std::cout << "implicit conversion allowed:" << c << i << '\n'; }};
callback cb6{ [] { /* "invalid argument types to callback" */ }};

Demo