Предположим, у меня есть этот код:
void ClassA::SomeFunction()
{
switch(something)
{
case case1:
doSomething();
break;
case case2:
doSomethingElse();
break;
.
.
.
}
...
void ClassB::SomeFunction()
{
switch(something) // this 'something' is the same 'something' as in
{ // the above class
case case1:
doSomethingCompletelyUnrelatedToClassAFunction();
break;
case case2:
doSomethingCompletelyUnrelatedToClassAOtherFunction();
break;
.
.
.
}
Две функции в двух классах делают совершенно разные вещи в одних и тех же случаях (все случаи в обоих коммутаторах абсолютно одинаковы).
По сути, я пишу небольшой эмулятор Chip-8, и два класса представляют мой «процессор» и дизассемблер.
Процессор должен декодировать код операции и делать некоторые вещи, в то время как дизассемблер должен просто форматировать строку на основе кода операции.
Я пытаюсь придумать способ избежать копирования / вставки всего переключателя и просто изменить функции, вызываемые в каждом случае.
Одно простое решение, которое я придумал, — это создание абстрактного класса, который обрабатывает код операции и имеет разные методы для каждого случая в коммутаторе. Классы CPU и Disassembler расширяют этот класс и реализуют свое поведение для каждого из методов. Однако я надеюсь на более элегантное решение, чем это.
Благодарю.
РЕДАКТИРОВАТЬ:
Согласно комментарию @Dogbert, я добавляю фрагмент реального кода:
wxString* DebugWindow::DisassembleOpCode(uint16_t opCode)
{
wxString disLine;
uint8_t nibble1 = (opCode & 0xF000) >> 12;
uint8_t nibble2 = (opCode & 0x0F00) >> 8;
uint8_t nibble3 = (opCode & 0x00F0) >> 4;
uint8_t nibble4 = (opCode & 0x000F);
if (opCode == 0x00E0)
disLine = "CLS";
else if (opCode == 0x00EE)
disLine = "RET";
else
switch (nibble1)
{
case 0x1:
{
// JMP nnn
disLine = wxString::Format("JMP %04x", nibble2 | nibble3 | nibble4);
break;
}
case 0x2:
{
// CALL nnn
disLine = wxString::Format("CALL %04x", nibble2 | nibble3 | nibble4);
break;
}
case 0x3:
{
// SE V[x], nn -- skip next instruction if V[x] == nn
disLine = wxString::Format("SE V[%x], %02x", nibble2, nibble3 | nibble4);
break;
}
...
...
...
case 0x8: // arithmetic operations between registers, see below
{
switch (nibble4)
{
case 0x0:
{
// LD V[x], V[y] -- sets value of register V[x] to value of register V[y]
disLine = wxString::Format("ADD V[%x], V[%x]", nibble2, nibble3);
break;
}
case 0x1:
{
// OR V[x], V[y] -- performs bitwise OR of values of registers V[x] and V[y], the result is stored in V[x]
disLine = wxString::Format("OR V[%x], V[%x]", nibble2, nibble3);
break;
}
...
...
Это часть функции DisassembleOpcode. Функция Step в классе CPU должна на самом деле делать вещи вместо форматирования строки, но состоит из тех же переключателей / вариантов.
В том же духе, как предложил dasblinkenlight, я бы даже сказал, что «табулирование» набора команд значительно улучшит читабельность проекта. Что-то вроде:
struct opcode instructionset[] = {
{ 0x1000, "JMP %04x", &do_jump},
...
Ваш код будет затем обращаться к таблице для каждой инструкции, возможно, таблица будет отсортирована по коду операции для быстрого поиска:
struct opcode op* = find(opCode, instructionset);
// for printing the instruction
disLine = wxString::Format(op->formatstring, opCode & op->format_mask);
Вы понимаете … Но ключевой момент: у вас будет хорошо отформатированная таблица вашего реализованного набора команд, которая, я думаю, значительно облегчит обслуживание / добавление инструкций в будущем.
В C вы можете заменить переключатель на массив указателей на функции. В C ++ у вас есть дополнительная опция использования указателей на класс с полиморфным поведением.
Вот как вы можете сделать это с помощью указателей на функции:
typedef void (*disassembly_action)(uint8_t,uint8_t,uint8_t,uint8_t);
static void doSomethingOpcodeZero(uint8_t,uint8_t,uint8_t,uint8_t);
static void doSomethingOpcodeOne(uint8_t,uint8_t,uint8_t,uint8_t);
static void doSomethingOpcodeTwo(uint8_t,uint8_t,uint8_t,uint8_t);
static disassembly_action disassembly[] = {
doSomethingOpcodeZero
, doSomethingOpcodeOne
, doSomethingOpcodeTwo
, ...
};
Теперь вы можете использовать индексирование массива вместо переключателя:
void ClassA::SomeFunction() {
uint8_t nibble1 = (opCode & 0xF000) >> 12;
uint8_t nibble2 = (opCode & 0x0F00) >> 8;
uint8_t nibble3 = (opCode & 0x00F0) >> 4;
uint8_t nibble4 = (opCode & 0x000F);
disassembly_action[something](nibble1, nibble2, nibble3, nibble4);
}
Вы можете создать аналогичный массив, чтобы заменить другой switch
, возможно, используя другую подпись для вашего action
функции.
Вот несколько быстрых подходов к использованию EventHandler для того, что вы можете зарегистрировать на маршрутизаторе, который что-то получает:
#include <iostream>
#include <vector>
#include <utility>
class SomethingHandler
{
public:
SomethingHandler() {}
~SomethingHandler() {}
virtual void handleSomething(int something)
{
doHandleSomething(something);
}
private:
virtual void doHandleSomething(int something) = 0;
};
class Router
{
public:
Router() {}
void registerHandlerForSomething(int somethingInteresting, SomethingHandler* h)
{
m_handler.push_back(std::pair<int,SomethingHandler*> (somethingInteresting, h));
}
void receive(int something) const
{
for(unsigned int i=0; i < m_handler.size(); ++i)
{
if(m_handler.at(i).first == something)
{
if(m_handler.at(i).second)
{
m_handler.at(i).second->handleSomething(something);
}
}
}
}
private:
std::vector< std::pair<int,SomethingHandler*> > m_handler;
};
class aHandler : public SomethingHandler
{
public:
aHandler() {}
private:
void doHandleSomething(int something)
{
std::cout << "handled something: " << something << "\n";
}
};
int main()
{
Router r;
aHandler h;
r.registerHandlerForSomething(42, &h);
r.receive(42);
}
это должно дать следующий вывод:
g++ main.cpp; ./a.out
handled something: 42
Вы можете изобрести специфичный для домена язык для описания операторов параллельного переключения, а затем преобразовать его в независимые операторы переключения с помощью сценария perl, вызываемого из Makefile.
Например:
BEGIN CASE 0x1
BEGIN FACET disassemble
// disassemble code for 0x1 goes here
END FACET
BEGIN FACET execute
// execute code goes for 0x1 here
END FACET
END CASE
BEGIN CASE 0x2
BEGIN FACET disassemble
// disassemble code for 0x2 goes here
END FACET
BEGIN FACET execute
// execute code for 0x2 goes here
END FACET
END CASE
Сценарий Perl может легко разделить аспекты на отдельные файлы:
disassemble.cc
case 0x1:
// disassemble code for 0x1 goes here
break;
case 0x2:
// disassemble code for 0x2 goes here
break;
execute.cc
case 0x1:
// execute code for 0x1 goes here
break;
case 0x2:
// execute code for 0x2 goes here
break;
Вы можете тогда #include
случаи в соответствующие контексты переключения.
Что-то вроде этого:
#!/usr/bin/perl
our %bindings;
our $case;
our $binding;
while(<>) {
if(/^\s*BEGIN CASE (.*)$/) {
die "Unexpected BEGIN CASE" if defined $case;
$case = $1;
}
elsif(/^\s*END CASE$/) {
die "Unexpected END CASE" if !defined $case or $binding;
undef $case;
}
elsif(/^\s*BEGIN FACET (.*)$/) {
die "Unexpected BEGIN FACET" if !defined $case or $binding;
$binding = {case => $case, code => []};
push @{$bindings{$1}}, $binding;
}
elsif(/^\s*END FACET$/) {
die "Unexpected END FACET" unless $binding;
undef $binding;
}
elsif($binding) {
push @{$binding->{code}}, $_;
}
}
for my $facet (sort keys %bindings) {
print "#ifdef FACET_", uc($facet), "\n";
for my $binding (@{$bindings{$facet}}) {
print "case ", $binding->{case}, ":\n";
print $_ for (@{$binding->{code}});
print "break;\n";
}
print "#endif\n";
}