面向对象的跳转 table 在多个 类 上定义

Object-oriented jump table defined over multiple classes

我正在将 NMOS6502 仿真器重构为多个 classes。我想知道是否有一种 "object-oriented" 方法来定义函数跳转 table。基本上,我已经定义了单独的指令 classes 来 class 化相关的 cpu 操作组——比如 "CStackInstHandler" 或 "CArithmeticInstHandler" 将引用 cpu对象。每条指令 class 都派生自抽象指令 class。每个派生指令 class 都有一组函数,这些函数将使用 cpu 对象的 public 接口来更改 cpu 状态,例如:

uint8_t opcode = _memory->readMem(_cpu->getProgramCounter());
AInstructionHandler* _handler = _cpu->getInstHandler(opcode);
_handler->setCpu(&cpu);
_handler->setMemory(&memory);
_handler->execute(opcode);    

问题在于,在运行时,需要使用操作码来确定指令处理程序以及为该处理程序定义的适当成员函数。

所以我们有 - 从内存中读取操作码,cpu 使用 table 将操作码映射到指令处理程序类型,然后指令处理程序使用相同的操作码来select 正确的函数。每条指令都会覆盖 "execute" 函数,例如:

void CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            this->BCC();
            break;
        case 0xb0:
            this->BCS();
            break;
        case 0xf0:
            this->BEQ();
            break;
        case 0x30:
            this->BMI();
            break;
        case 0xd0:
            this->BNE();
            break;
        case 0x10:
            this->BPL();
            break;
        default:
            break;
    }

}

void CBranchInstHandler::BCC() {
    uint16_t address = this->getAddress();
    if(!_cpu->isCarry()) {
        uint16_t pc = _cpu->getPC();
        pc += address;
        _cpu->setPC(pc);
    }
}

/*more instruction specific functions...*/

我最终进行了两次查找,其中一次是多余的。一个到 select 处理程序,另一个到 select 处理函数。我觉得这是完成此任务的错误方法,但我不确定是否有一种替代方法不仅可以分解为非成员函数组。

我想知道是否有人对这个问题有所了解。它基本上归结为想要将 class 重构为更小的部分(cpu class 指令成员函数重构为 cpu class 和指令 classes),但是所有的组件都是相互关联的,以至于我最终不得不重复自己。引入了冗余。

非面向对象的解决方案是让这些指令成为接受 cpu 引用的非成员函数。然后,将定义一个函数 jump table ,指令将由操作码查找和索引并执行。

这对于对象来说似乎并不实用。我可以将所有说明设为静态或其他内容,但这似乎没有抓住要点。

任何关于即使是无关紧要的问题的见解或信息都会非常有帮助。

谢谢。

您可以使用指向 class 成员的指针 function/method:

void (CBranchHandlerBase::*)();

使用 for 存储指向给定 _opcode 应调用的方法的指针。

map<uint8_t, void (CBranchHandlerBase::*)()> handlers;
handlers[0x90] = &BCC;
handlers[0xb0] = &BCS;
...

上面的代码应该在处理程序的基础 class 中的初始化 section/method 中提供。当然,BCC、BCS 等必须声明为纯虚方法才能使该方法起作用。

然后代替你的开关:

void CBranchHandlerBase::execute() {
    (this->*handlers[_opcode])();
}

请注意,execute 是在基 class 中定义的(它不必是虚拟的!因为每个处理程序都将具有与 execute 方法相同的功能)。

编辑:实际上可以用向量或数组替换地图:2^(8*sizeof(uint8_t))出于效率原因

据我所知,您正在做的是为每种类型的指令(分支、算术、加载、存储等)创建一个 class,然后在其中为各个指令编写成员函数-- c.f。你有一个 "CBranchInstrHandler" 可以处理 "branch on carry"、"branch on zero" 等?

完全面向对象的方法是将您的子class扩展为单独的指令。

class CBranchInstrHandler { virtual void execute() = 0; };
class CBranchOnCarryClear : public CBranchInstrHandler {
    void execute() override {
        ...;
    }
};
class CBranchOnCarrySet : public CBranchInstrHandler {
    void execute() override {
        ...;
    }
};

现在您可以一次查看您的说明,但您需要所有这些的一对一映射。

switch (opCode) {
    case 0x90: return .. CBranchOnCarryClear .. ;
    case 0xB0: return .. CBranchOnCarrySet .. ;
}

省略号是因为我不确定您是如何获得指向 CBranchInstrHandler 的指针的;我猜它们是静态的,你不会 new 对它们进行每条指令。

如果它们是无数据的,您可以 return 它们按值作为函数对象:

struct Base { virtual void execute() { /* noop */ } };
struct Derived { void execute(override) { ... } };

Base getHandler(opcode_t opcode) {
    if (opcode == 1) { return Derived(); }
}

但我怀疑您可能想要获取参数并存储状态,在这种情况下,return 按此处的值可能会导致切片。

当然,如果您使用的是 C++11,则可以使用 lambda:

switch (opCode) {
    case 0x90: return [this] () {
        ... implement BCC execute here ...
    };
    case 0xB0: return [this] () {
        ... implement BCS execute here ...
    }
}

我要将我的评论推广到一个答案:object-oriented 解决方案,如您所说,让 child classes 完全负责决定哪些操作码他们回应。

我建议最简单的方法不是尝试构建 two-stage switch,而是简单地将每个操作码路由到每个 child 并让child 贡献与否。这是最小可行的解决方案。

如果您需要优化,那么最简单的方法就是重新制定:

void CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            this->BCC();
            break;
            ... etc ...
    }
}

收件人:

FuncPtr CBranchInstHandler::execute() {
    switch(_opcode) {
        case 0x90:
            return BCC;
            ... etc ...
    }
    return NULL;
}

因此每个 execute returns 它是否确实处理了该操作码。

在 parent class 中,您可以简单地保留从操作码到函数指针的 table。一个数组就可以了。 table 最初将始终包含 NULL

执行操作码时,在 table 中查找处理程序。如果处理程序在那里,调用它并继续。如果不是,则依次在每个 child 上调用 execute,直到有人 returns 处理程序,然后将它们放入 table,然后调用它。因此,您将在 运行 时构建它 just-in-time。每个操作码的第一个 运行 将花费稍长的时间,但随后您将获得相当于一个跳跃 table.

这样做的好处是,它允许有关 child 处理的信息在语法上与实际处理紧密相关,从而减少代码开销和出错的可能性。