0

我正在将 NMOS6502 仿真器重构为多个类。我想知道是否有一种“面向对象”的方式来定义函数跳转表。基本上,我已经定义了单独的指令类来对相关的 cpu 操作组进行分类——比如“CStackInstHandler”或“CArithmeticInstHandler”,它们将引用一个 cpu 对象。每个指令类都派生自一个抽象指令类。每个派生指令类都有一组函数,它们将使用 cpu 对象的公共接口来改变 cpu 状态,例如:

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

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

所以我们有 - 从内存中读取操作码,cpu 使用一个表将操作码映射到指令处理程序类型,然后指令处理程序使用相同的操作码来选择正确的函数。每条指令都会覆盖一个“执行”功能,例如:

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...*/

我最终得到了两个查找,其中一个是多余的。一个选择处理程序,另一个选择处理程序函数。我觉得这是完成这项任务的错误方法,但我不确定是否有一种替代方法不仅会演变为非成员函数组。

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

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

对于对象,这似乎并不实际。我可以将所有指令设为静态或其他东西,但这似乎没有抓住重点。

任何有关甚至切向相关问题的见解或信息都会非常有帮助。

谢谢。

4

3 回答 3

1

您可以使用指向类成员函数/方法的指针:

void (CBranchHandlerBase::*)();

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

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

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

然后代替您的开关:

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

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

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

于 2015-06-01T14:18:27.300 回答
1

如果我理解,您正在做的是为每种类型的指令(分支、算术、加载、存储等)创建一个类,然后在这些指令中为各个指令编写成员函数 - cf 您有一个“CBranchInstrHandler”哪个处理“携带分支”,“零分支”等?

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

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在每条指令中都对它们进行处理。

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

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

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

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

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

switch (opCode) {
    case 0x90: return [this] () {
        ... implement BCC execute here ...
    };
    case 0xB0: return [this] () {
        ... implement BCS execute here ...
    }
}
于 2015-06-01T14:35:48.510 回答
1

我将把我的评论推广到一个答案:正如你所说,面向对象的解决方案是让子类完全负责决定他们响应哪些操作码。

我建议最简单的方法不是尝试构建一个两阶段switch,而是简单地将每个操作码路由到每个孩子,让孩子要么贡献,要么不贡献。这是最小可行的解决方案。

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

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

到:

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

因此,每个execute返回它是否实际上处理了该操作码。

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

执行操作码时,请在表中查找处理程序。如果处理程序在那里,请调用它并继续。如果不是,则依次调用execute每个孩子,直到有人返回处理程序,然后将它们放入表中,然后调用它。因此,您将在运行时即时构建它。每个操作码的第一次运行将花费稍长的时间,但随后您将拥有相当于跳转表的内容。

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

于 2015-06-02T20:54:33.730 回答