Continuing OnI last left off with a very simple example of a machine capable of outputting some text. That was all it could do, and it was always the same text. If you remember, last time I spoke about the difference between programs built in this static manner, and programs that are able to handle more dynamic situations. If it were really necessary to create a different type of instruction for every type of message you wanted to output, it could end up being a nightmare. The Benefit of DataThe simplest remedy to this situation is to create a new style of instruction that makes use of optional data to dictate the message you would like printed. With this type of instruction, all that would be necessary to print a custom message would be to assign it the proper data. No need for hordes of specialized instruction types. So now we will add support in our Instruction class for using additional data: // the basic instruction class Instruction { public: Instruction(opcode code) : _code(code), _data(0) {} Instruction(opcode code, const char* data, size_t dataSize) : _code(code), _data(new char[dataSize]) { memcpy(_data, data, dataSize); } ~Instruction() { delete[] _data; } opcode Code() const { return _code; } const char* Data() const { return _data; } // read the data private: opcode _code; char* _data; // additional data }; While creating an instruction, additional data can be paired with an opcode by using the second form of constructor. This constructor allocates memory of the correct length to store this data and then copies the source data into its own private storage. This data can be read, but will never be changed again, according to the current interface. A destructor has been added to handle deletion of the data. If you're asking why the constructor creates a copy of the data provided when it seems simple enough just to assign the internal pointer to the address of the data provided, consider this: What would happen if the source data were to leave scope? You would be left with a dangling pointer. This is why the class owns its data buffer. Now, we would like to add a new opcode to designate the new functionality we require: enum opcode { op_talk, op_print, // our new printing code op_end }; The last new inclusion to make is in the virtual machine's processing loop. In the case of our new opcode, it must print the message described by the data, and then go to the next instruction: void VirtualMachine::Execute(size_t scriptId) { SelectScript(scriptId); // select our _instrPtr by script ID _instr = _instrPtr; // set our iterator to the beginning while (_instr) { switch(_instr->Code()) { case op_talk: std::cout << "I am talking." << std::endl; ++_instr; // iterate break; case op_print: std::cout << _instr->Data() << std::endl; // print data ++_instr; // iterate break; case op_end: _instr = 0; // discontinue the loop break; } } } It would be a good idea to make sure things work correctly. In our main source, we will test the new instruction. All we need is some data to print, which we then pass to the printing instruction's constructor, along with its proper length (the string length + 1 for the terminating null character): VirtualMachine vm; // simulate some external data char* buffer = "this is printed data"; // build the script vector<Instruction> InstrList; InstrList.push_back(Instruction(op_talk)); // talk still works the same way InstrList.push_back(Instruction(op_print, buffer, strlen(buffer)+1)); // print InstrList.push_back(Instruction(op_end)); // then end Script script(InstrList); // load the script and save the id size_t scriptID = vm.Load(script); // execute the script by its id vm.Execute(scriptID); If all is in working order, this code should talk, and then print the message provided by the data.
|