c++ 通过插件接口公开变量函数

jpfvwuh4  于 2023-03-20  发布在  其他
关注(0)|答案(1)|浏览(115)

bounty将在3天后过期。回答此问题可获得+50的声誉奖励。joergbrech正在寻找来自声誉良好来源的答案

我有一个框架,它有一个类型擦除的函数类型Function,它将std::any示例Map到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;
}

https://godbolt.org/z/aT4n36v6M
注意,为了解决这个问题,这个例子被简化了 (没有错误检查,真实的的代码没有使用std::any,而是使用了一个适当的反射库,它不限于函数指针和std::function,main函数的第一部分将作为插件的一部分来实现......)
代码可以很好地处理非模板函数。但是现在我想让用户在插件中提供变量函数。我意识到在当前的设置中这是不可能的,因为模板是在调用点初始化的,而不是作为插件的一部分编译的。

有没有办法让用户在这种框架中注册可变变量函数?

我对所有的解决方案都是开放的,只要现有的功能保持不变,包括API在内的代码的任何部分都可以修改。
唯一的要求是,注册的函数是 truely variadic:我知道我可以在插件中显式地示例化变量函数的1,2,3,......,n个参数,并分别注册每个示例化。

j7dteeu8

j7dteeu81#

正如问题中所述,对于template<typename ...T> void for(T...);可变参数函数(或一般情况下-任何函数模板),您可以注册它的任何示例-例如
fwk.register("foo_ifi", &foo<int,float,int>);
这就是我们所能做的,除非问题是不提供模板参数,否则你可以使用lambda来 Package 你的函数模板:

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

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

固定参数数的解决方案:

如果你能限制你的框架的参数和类型的最大数量--那么这是可行的--参见an example
只有几个助手类:

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

}

而框架本身--通过一些模板魔术、元组、变量、函数和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)...);
    }

};

它的作用证明:

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

任意数量参数的解:

如果你想注册这个函数模板本身,使它可以被任何参数调用-你需要使框架也成为一个知道它的“函数”的模板。

  • 注意:所有这些都需要C20,但在C17甚至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)...);
    }
};

为了使注册变得简单一些,需要一些帮助类:

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

和一些例子-如何使用它:

// 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>{}>;

正如你所看到的--这样的变量需要被包含在lambda中--因为只有lambda是我们可以提供的非模板的东西--然而它的调用操作符是一个方法模板。
以及它的工作原理:

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

相关问题