有没有办法在 C++ 中创建组合函数编译时数组?

Is there a way to create an array of combined functions compile-time in c++?

我正在使用 C++ 开发 NES 模拟器,我认为 运行 操作码最有效的方法是调用函数数组中的函数指针,这些函数指针完全执行操作码的功能。

问题是每个操作码都有特定的操作和内存地址。在寻找解决方案时,我偶然发现了 lambda 表达式。这对于现代硬件上的 NES 模拟器来说绝对足够好。但是,我找不到解决方案,使数组中的每个函数都包含用于操作和寻址的机器代码,而无需定义 256 个单独的函数。

这就是我对结合 f 和 g 的类似函数的想法:

int addone(int x) {
  return x + 1;
}

int multtwo(int x) {
  return 2 * x;
}

something combine(function<int(int)> f, function <int(int)> g) {
  /* code here */
}

/*
combine(addone, multtwo) creates a function h that has the same machine code as
int h(x) {
  return 2 * x + 1;
}
*/

有什么想法吗?如果非要我猜的话,它应该与模板有关。谢谢!

您可以这样做,使用 lambda 捕获两个函数并将函数分配给变量:

#include <functional>
#include <iostream>


int addone(int x){
    return x + 1;
}

int multtwo(int x){
    return x * 2;
}

std::function<int(int)> combine(std::function<int(int)> f, std::function<int(int)> g){
    auto tmp = [&](int x){ return f(g(x)); };
    return std::function<int(int)>(tmp);
}

int main(){
    auto h = combine(std::function<int(int)>(addone), std::function<int(int)>(multtwo)); // (2 * x) + 1
    std::cout << h(10); // Prints 21
}

如果你想让它统一功能,可以使用模板:

#include <functional>
#include <iostream>


int addone(int x){
    return x + 1;
}

int multtwo(int x){
    return x * 2;
}

template <typename Func>
std::function<Func> combine(std::function<Func> f, std::function<Func> g){
    auto tmp = [&](int x){ return f(g(x)); };
    return std::function<Func>(tmp);
}

int main(){
    auto h = combine<int(int)>(std::function<int(int)>(addone), std::function<int(int)>(multtwo));
    std::cout << h(10) << "\n"; // Prints 21
}

您也不需要指定类型,因为编译器可以计算出来:

#include <functional>
#include <iostream>


int addone(int x){
    return x + 1;
}

int multtwo(int x){
    return x * 2;
}

template <typename func>
std::function<Func> combine(std::function<Func> f, std::function<Func> g){
    auto tmp = [&](int x){ return f(g(x)); };
    return std::function<Func>(tmp);
}

int main(){
    auto h = combine(std::function<int(int)>(addone), std::function<int(int)>(multtwo));
    std::cout << h(10) << "\n"; // Still prints 21
}

如果您想自动创建函数,请使用 2pichar 的答案和 for 循环,但对于模拟器,您可能需要类似 opcode->int(*)(int) 的东西。这可以通过一些 tree-like 结构来完成:

std::map<char, naive_opcode> opcodes;
struct naive_opcode {
    std::map<char, naive_opcode> next;
    int(* opcode_func)(int);
};

您可以通过以下方式解决此问题:

char data;
buf >> data;
naive_opcode opcode = opcodes[data];
while(!opcode.opcode_func){
    buf >> data;
    opcode = opcode.next[data];
}
opcode.opcode_func(param);

这当然会忽略错误,并且不包括指令指针和 .text 段内存之类的东西,而是将其替换为 buf 缓冲区以用于说明目的(在现实生活中的示例中,我希望这会被 data=memory[ip]; ++ip; 取代)。然后可以将其与以下实现相结合:

#include <iostream>


int addone(int x){
    return x + 1;
}

int multtwo(int x){
    return x * 2;
}

template<int(* F)(int), int(* G)(int)>
int combined(int x){
    return F(G(x));
}

int main(){
    std::cout << combined<addone,multtwo>(10);
}

你基本上可以将 naive_opcode 的结束节点定义为 {{}, combined<addone,multtwo>}.

不幸的是,正如我在评论中提到的,这可能无法自动完成。我侦察的你能做的最好的事情就是定义如下内容:

std::vector<std::pair<const char*, int(*)(int)>> raw_opcodes = {{"\x10\x13", addone}, ...};

然后将其解析为树状结构。作为一个简短的旁注:如果所有操作码都是 1 字节(我不确定,因为我不熟悉 NES,所以可能不需要)。然后一个简单的 std::map<char,int(*)(int)> opcodes 就足够了,而不是复杂的 naive_opcode (或更好的树)实现。

查了一下,你似乎不需要树实现,但像这样的修改可能会有用:

template<int(* F)(int)>
int combined(int x){
    return F(x);
}

template<int(* F)(int), int(* A)(int), int(*... G)(int)>
int combined(int x){
    return F(combined<A, G...>(x));
}

这允许将许多效果相互组合,而不是 2 个。

我们可以使用模板创建通用 compose 函数,该函数使用捕获传入函数的 lambda 和 returns “组合”两个一元函数。

#include <functional>
#include <iostream>

template <typename Input, typename Output1, typename Output2>
std::function<Output2(Input)> compose(
    std::function<Output2(Output1)> f, 
    std::function<Output1(Input)> g
) {
    return [&f, &g](Input x) { return f(g(x)); };
}

int foo(int x) {
    return x + 1;
}

int bar(int x) {
    return x * 2;
}

int main() {
    auto baz = compose<int, int, int>(foo, bar);

    std::cout << baz(5) << std::endl;

    auto wooble = compose<int, int, float>(
        [](int x) { return static_cast<float>(x) + 1.5; },
        [](int x) { return x * 3; }
    );

    std::cout << wooble(5) << std::endl;   

    return 0;
}

你想要这个吗?

int f1(int x) { return x + 1; }
int f2(int x) { return x * 2; }
int f3(int x) { return x * 3; }
int f4(int x) { return x - 5; }
int f5(int x) { return x + 9; }

int main() {
    auto cf = combine<int>(f1, f2, f3, f4, f5);
    std::cout << cf(5) << std::endl;
    return 0;
}
Output:
40

完整代码:

#include <functional>
#include <concepts>
#include <iostream>

template<typename T, typename NUM = int>
concept num_processor = requires (T t, NUM x) {
    {t(x)} -> std::same_as<NUM>;
};

template<typename NUM, num_processor p>
NUM process(NUM v, p proc) {
    return proc(v);
}

template<typename NUM, num_processor p, num_processor... Funs>
NUM process(NUM v, p proc, Funs... funs) {
    return process(proc(v), funs...);
}

template<typename NUM, num_processor... Funs>
std::function<NUM (NUM)> combine(Funs... funs) {
    return [...funs = funs] (NUM v) { return process(v, funs...); };
}

int f1(int x) { return x + 1; }
int f2(int x) { return x * 2; }
int f3(int x) { return x * 3; }
int f4(int x) { return x - 5; }
int f5(int x) { return x + 9; }

int main() {
    auto cf = combine<int>(f1, f2, f3, f4, f5);
    std::cout << cf(5) << std::endl;
    return 0;
}

编译为 -std=c++20 用于 gcc,/std:c++latest 用于 msvc

我想说的是,当你想为函数编写泛型时,切换到仿函数是一种“设计模式”:编译器旨在轻松处理类型,但处理你想要的东西的函数指针mis-match 并保持优化 compile-time 变得丑陋!

所以我们要么将我们的函数写成仿函数,要么我们包装它们为仿函数:

struct A
{
    static constexpr int Func (int x)
    {
        return -3*x + 1;
    }
};

struct B
{
    static constexpr int Func (int x)
    {
        return -2*x - 5;
    }
};

// etc...

如果我们在使用它们的方式上有很好的对称性,那么我们就可以系统地管理它们。例如。如果我们总是想像f(g(h(...y(z())...)))那样把它们组合起来,那么我们可以这样解决:

template <class T, class ... Ts>
struct Combine
{
    static constexpr int Get ()
    {
        int x = Combine<Ts...>::Get();
        return T::Func(x);
    }
};

template <class T>
struct Combine <T> // The base case: the last function in the list
{
    static constexpr int Get ()
    {
        return T::Func();
    }
};

demo

或者,如果我们没有这样的运气,我们将不得不像您建议的那样求助于更多 old-fashioned 输入:

template <class Funcs, class Data>
constexpr int Combine (const Data & d) 
{
    Funcs F;
    // Some use without much symmetry:
    return F.f(F.g(d)) - F.h(d);
}

int main ()
{
    struct FuncArgs
    {
        A f;
        B g;
        C h;
    };

    return Combine<FuncArgs>(5);
}

demo

请注意,在第二个示例中,我已将静态方法更改为 non-static。这并不重要——无论如何,编译器都应该完全优化这些,但我认为在这种情况下,它会使语法稍微好一点(并显示另一种风格)。

大多数其他答案建议 std::function,但我担心它需要的运行时开销。

由于您不需要 select 哪些函数是在运行时组合的,因此您可以不用它。我使用与 相同的想法,但针对任意类型进行了概括,并希望使用更好的语法:

#include <iostream>
#include <utility>

template <auto F0, auto ...F>
struct FuncList
{
    static constexpr auto first = F0;
    static constexpr bool have_next = true;
    using next = FuncList<F...>;
};
template <auto F0>
struct FuncList<F0>
{
    static constexpr auto first = F0;
    static constexpr bool have_next = false;
};

template <typename F, typename ...P>
decltype(auto) Combine(P ...params) // Normally there would be `&&`, but I removed it allow casting to any function pointer type.
{
    if constexpr (F::have_next)
        return F::first(Combine<typename F::next, P &&...>(std::forward<P>(params)...));
    else
        return F::first(std::forward<P>(params)...);
}

int addone(int x)
{
    return x + 1;
}

int multtwo(int x)
{
    return 2 * x;
}

int main()
{
    int (*ptr)(int) = Combine<FuncList<addone, multtwo>>;
    std::cout << ptr(10) << '\n'; // 21
}