通过插件接口公开可变参数函数

joe*_*ech 7 c++ plugins functional-programming variadic-templates c++17

我有一个框架,它有一个类型擦除的函数类型Function,将std::any实例映射到std::any实例和函数注册表std::unordered_map<std::string, Function>。该框架有一个插件系统,插件作者可以在此函数注册表中注册函数。这样,新功能可以在运行时集成到框架中。

该框架的一个非常精简的实现可能如下所示:

#include <any>
#include <vector>
#include <memory>
#include <functional>
#include <iostream>

class Function
{
public:

    template <typename R, typename... Args>
    Function(R(*f)(Args...)) : impl(std::make_unique<FunctionImpl<R, Args...>>(f)) {}


    template <typename... Args>
    std::any operator()(Args&&...args) const
    {
        std::vector<std::any> args_vec({std::forward<Args>(args)...});
        return impl->invoke(args_vec);
    }

private:

    struct FunctionProxy {
        virtual std::any invoke(std::vector<std::any>&) const = 0;
        virtual ~FunctionProxy(){}
    };

    template <typename R, typename... Args>
    class FunctionImpl : public FunctionProxy
    {
    public:
        FunctionImpl(R(*f)(Args...)) : fun{f} {}

        std::any invoke(std::vector<std::any>& args) const override
        {
            return invoke_impl(args, std::make_index_sequence<sizeof...(Args)>{});
        }
    private:
        template <size_t... I>
        std::any invoke_impl(std::vector<std::any>& args, std::index_sequence<I...>) const
        {
            if constexpr (std::is_void_v<R>) {
                fun(std::any_cast<Args>(args[I])...);
                return std::any{};
            } else {
                return std::any(fun(std::any_cast<Args>(args[I])...));
            }
        }

        std::function<R(Args...)> fun;
    };

    std::unique_ptr<FunctionProxy> impl;
};

class FunctionRegistry
{
public:
    template <typename R, typename... Args>
    void register_function(R(*fun)(Args...), std::string name)
    {
        functions.emplace(std::make_pair(name, Function(fun)));
    }

    Function const& resolve(std::string const& name){
        return functions.at(name);
    }
private:
    std::unordered_map<std::string, Function> functions;
};

/***********************************************************************************/

double add(double x, double y) {
    return x+y;
}

template <typename... Args>
double sum(Args... args) {
    return (0 + ... + args);
}

int main()
{
    // code called in void init() function provided by plugin
    FunctionRegistry functions;
    functions.register_function(&add, "add");

    //....

    // code called in the framework at runtime
    auto const& f = functions.resolve("add");
    std::cout << std::any_cast<double>(f(5.,3.));
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

https://godbolt.org/z/aT4n36v6M

请注意,为了解决这个问题,示例被简化了(没有错误检查,真正的代码不使用std::any而是使用适当的反射库,它不限于函数指针和 std::function,主函数的第一部分将作为插件的一部分实现......)

该代码适用于非模板函数。但现在我想让用户能够在插件中提供可变参数函数。我意识到在当前设置中这是不可能的,因为模板是在调用站点初始化的,而调用站点没有编译为插件的一部分。

有什么方法可以允许用户在这种框架中注册可变参数函数吗?

我对所有解决方案持开放态度,只要现有功能保持不变,包括 API 在内的代码的任何部分都可以更改。

唯一的要求是,注册的函数是真正的可变参数:我知道我可以在插件中显式实例化 1,2,3,....,n 个参数的可变参数函数,并单独注册每个实例化。

Pio*_*ycz 2

正如问题中已经指出的,对于template<typename ...T> void for(T...);可变参数函数(或一般来说 - 任何函数模板),您可以注册它的任何实例 - 就像

fwk.register("foo_ifi", &foo<int,float,int>);

这就是它所能做的一切。除非问题是不提供模板参数 - 那么你可以使用 lambda 来包装你的函数模板:

template <typename ...T> auto foo(T... x) { .... }

std::function<void(int,int,int)> f = 
 [](auto ...x) ->decltype(auto) { return foo(x...); };
Run Code Online (Sandbox Code Playgroud)

固定数量参数的解决方案:

如果您可以将框架限制为某个最大参数数量和某个固定数量的类型 - 那么这是可行的 - 请参阅示例

只有几个辅助类:

namespace detail
{
template <typename, typename>
struct FunctionBuilder;
template <std::size_t ...I, typename V>
struct FunctionBuilder<std::index_sequence<I...>, V>
{
    using type = std::function<V(decltype(I,std::declval<V>())...)>;
};

template <typename, typename>
struct MapsBuilder;
template <std::size_t ...I, typename V>
struct MapsBuilder<std::index_sequence<I...>, V>
{
    using type = std::tuple<
        std::map<std::string, 
                 typename FunctionBuilder<std::make_index_sequence<I>, V>::type>...>;
};

}
Run Code Online (Sandbox Code Playgroud)

框架本身 - 通过一些模板魔法、元组、变体、函数和 lambda - 它几乎可以自动完成:


template <std::size_t MaxArg, typename ...T>
struct Framework {
    using Value = std::variant<T...>;

    using Maps = detail::MapsBuilder<std::make_index_sequence<MaxArg>, Value>::type;
    Maps maps;

    template <typename R, typename ...A>
    void registerFunction(std::string name, R (*fun)(A...))
    {
        std::get<sizeof...(A)>(maps)[std::move(name)] = [fun](auto ...x) -> Value {
            return fun(std::get<A>(x)...);
        };
    }

    template <std::size_t N, typename Callable>
    void registerCallable(std::string name, Callable fun)
    {
        std::get<N>(maps)[std::move(name)] = [fun](auto ...x) -> Value {
            return std::visit([&](auto ...a) -> Value { return fun(a...); },  x...);
        };
    }

    template <typename Callable>
    void registerVariadicCallable(std::string name, Callable fun)
    {
        auto funForValues = [fun](auto ...x) -> Value {
            return std::visit([&](auto ...a) -> Value { return fun(a...); },  x...);
        };
        std::apply([&](auto& ...m) { ((m[name] = funForValues), ...); }, maps);
    }

    template <typename ...A>
    Value call(std::string const& name, A ...a) {
        return std::get<sizeof...(A)>(maps)[name](Value(a)...);
    }

};
Run Code Online (Sandbox Code Playgroud)

以及它有效的证明:

template <typename ...T>
auto sum(T ...a) {
    return (a + ... + 0);
}

int sum2(int a, int b)
{
    return a + b;
}


int main() {
    Framework<5, int, float> fwk;
    fwk.registerFunction("sum2", sum2);
    fwk.registerCallable<2>("sum2t", [](auto ...x){ return sum(x...); });
    fwk.registerVariadicCallable("sum", [](auto ...x){ return sum(x...); });

    auto r1 = std::get<int>(fwk.call("sum2", 1, 2));
    auto r2 = std::get<float>(fwk.call("sum2t", 1, 2.1f));
    auto r3 = std::get<float>(fwk.call("sum", 1, 2.1f, 3));
    std::cout << r1 << '\n';
    std::cout << r2 << '\n';
    std::cout << r3 << '\n';
}



Run Code Online (Sandbox Code Playgroud)

任意数量参数的解决方案:

如果您想注册此函数模板本身,使其可调用任何参数 - 您需要使框架也是一个知道其“函数”的模板。像这样的东西

注意:所有这些都需要 C++20,但在 C++17 甚至 C++11 中也是可行的 - 但需要更多的努力。

template <auto NamedCallable, auto ...NamedCallables>
class Framework;

template <auto NamedCallable>
class Framework<NamedCallable> {
public:
    template <typename ...T>
    static decltype(auto) call(std::string_view name, T... params)
    {
        if (NamedCallable.name == name)
            return NamedCallable(std::forward<T>(params)...);
        throw std::runtime_error("Not found caller: " + std::string(name));
    }
};

template <auto NamedCallable, auto ...NamedCallables>
class Framework : Framework <NamedCallables...> {
public:
    template <typename ...T>
    static decltype(auto) call(std::string_view name, T... params)
    {
        if (NamedCallable.name == name)
            return NamedCallable(std::forward<T>(params)...);
        return Framework<NamedCallables...>::call(name, std::forward<T>(params)...);
    }
};
Run Code Online (Sandbox Code Playgroud)

为了使注册更容易一些 - 需要一些辅助类:

template<size_t N>
struct StringLiteral {
    constexpr StringLiteral(const char (&str)[N]) {
        std::copy_n(str, N, value);
    }
    constexpr operator std::string_view() const { return value; }
    
    char value[N];
};

template <auto Name, auto Callable>
struct NamedCallable
{
    static constexpr std::string_view name = Name;
    template <typename ...T>
    decltype(auto) operator()(T... a) const {
        return Callable(std::forward<T>(a)...);
    }
};
Run Code Online (Sandbox Code Playgroud)

一些例子 - 如何使用它:

// A variadic function
template <typename ...T>
void foo(T... a) {
    std::cout << "foo:";
    ((std::cout << " " << a), ...);
    std::cout << "\n";
}
// A regular function
void bar(int a, int b, int c) {
    std::cout << "bar: " << a << ' ' << b << ' ' << c << '\n';
}

using MyFramework = Framework<
    NamedCallable<StringLiteral{"foo"}, [](auto ...x) { return foo(x...); }>{},
    NamedCallable<StringLiteral{"bar"}, bar>{}>;
                             
Run Code Online (Sandbox Code Playgroud)

正如您所看到的 - 无论如何,这样的变量需要包含在 lambda 中 - 因为只有 lambda 是我们可以作为非模板事物提供的东西 - 然而它的调用运算符是一个方法模板。

以及它是如何工作的:

int main() {
    MyFramework fwk;
    fwk.call("foo", 1, 2, 3);
    fwk.call("bar", 1, 2, 3);
}

Run Code Online (Sandbox Code Playgroud)