OOP is not at this time supported in NXC. Even though the memory management in the standard firmware might be limiting it is still possible to achieve a simple OOP implementation fairly easily. It will however be a bit inefficient memory wise and you will have the standard issues with tread-safety.
However actually testing my hypothesis in the large scale turned out to be cumbersome and thus I didn’t… If you notice anything that wouldn’t work out, something I forgot to consider or anything, please leave a comment : )
Object representation
The base for every object is a data structure containing all non-static variables. Lets consider a class like this:
class monster{ public: static const unsigned char LEVEL_EASY = 0; static const unsigned char LEVEL_MEDIUM = 1; static const unsigned char LEVEL_HARD = 2; protected: unsigned char level; unsigned int hp; unsigned int max_hp; unsigned char attack[]; public: monster(); ~monster(){ //The destructor could reset large arrays ArrayInit( attack, 0, 0 ); } unsigned char difficulty() const{ return level; } float life() const{ return (float)hp/max_hp; } void receive_damage( unsigned char power, unsigned char attacker_level ); void attack( monster opponent, unsigned char attack_id ); };
It would end up to something like this in NXC code:
const byte monster::LEVEL_EASY = 0; //Yes, I know you can't do it like this in NXC... const byte monster::LEVEL_MEDIUM = 1; const byte monster::LEVEL_HARD = 2; struct monster{ byte level; unsigned int hp; unsigned int max_hp; byte attack[]; }; monster monster_paratemp;
The static variables are in global scope and the struct contains the other variables, nothing special here. Then an instantiation of the object is created, this will serve as a temporary object for function calls explained below.
Functions
At the NBC level, all variables are global and every reference to a variable is static and can’t be changed. Dynamic memory (other than arrays which content is dynamic) and pointers aren’t supported. This means that functions operate on a fixed set of variables and that all parameters must be passed by copying. If you want to pass a parameter by reference it is actually passed by copying and then copied back once the function returns.
This is problematic since in order to share the same function for multiple instances of the object we will need to tell the function which object to act upon, in short we need to pass the object by either reference or constant reference.
object.life(); //The way we want to call it in code life( object ); //The way it would be implemented behind the scenes
NXC currently makes a copy for each function however this isn’t optimal if we have many functions in the object. Even if we have a lot of free memory and doesn’t care about wasting some, it will have a rather negative effect on the file size of the .rxe file due to the way variables are declared.
To avoid making this effect to large, I suggest sharing a single object for all functions. It could also optimize repeated function calls in some cases as you could leave the copy in the function copy instead of copying it back and forth. (It will cause longer delays if you need to protect it with a mutex though.) This is the reason for the monster_paratemp variable in the example.
(It would be nice to see this form for optimization for general functions, though it would be a lot more complicated since you need to take treading and sub-functions into consideration…)
If there is only one instance of the class, then the functions should of course operate on the object directly instead of using a copy. Inline functions should do the same. And if the class only contains inline or static functions, a copy shouldn’t be needed at all.
Inheritance
Here is an example where the monster class is inherited:
class magic_monster: public monster{ private: unsigned int sp; unsigned int max_sp; public: magic_monster(): monster(){ ArrayBuild( attack, attack, 45, 30 ); } ~magic_monster(){ ArrayInit( attack, 0, 0 ); } float magic_power() const{ return (float)sp/max_sp; } void attack( monster opponent, unsigned char attack_id ); //The attack might be magic };
The NXC struct:
struct magic_monster{ monster parent_monster; unsigned int sp; unsigned int max_sp; }; magic_monster magic_monster_paratemp;
Instead of copying the monster struct and then add the extra variables, the struct is included as a sub-element. The reason is that this makes it a lot easier to call the functions defined in the inherited class. To call those, you simply copy parent_monster into monster_paratemp, call the function and then copy monster_paratemp back into parent_monster if necessary.
Multi-inheritance should also be easily possible this way, but I have never used it before so I don’t know if there are any special considerations to take…
Virtual functions
Unfortunately there is no way to properly implement virtual functions at runtime. It must be done completely at compile-time to work. (Well, fairly simple virtual functions could work, but the implementation would be inefficient and nowhere close to being sufficient.)
However because pointers aren’t supported, the cases where you need to save it in a parent class (and therefore having to rely on runtime detection) are rather few. There are only two cases I can think of. First one being function calls which accepts an object. And second one being the use of virtual functions directly in the base class, it would always use its own implementation (if available).
I think that we should depend on compile-time virtual functions, accept the changed functionality in the cases where it can’t be detected, but try to warn the programmer about issue. (Perhaps a custom keyword could be added to turn off the warning to show to compiler that you understand the issue?)
Functions accepting an object of the same class
One issue that appeared of using a single copy for the object parameter is apparent in the following example:
void monster::attack( monster opponent, unsigned char attack_id ){ opponent.receive_damage( attack[attack_id], level ); } monster dragon, ogre; dragon.attack( ogre, 2 );
If we try to add how the temporary object is used together with the function calls it looks like this:
monster_paratemp = dragon; attack( ogre, 2 ); monster_paratemp = ogre; receive_damage( monster_paratemp.attack[2], monster_paratemp.level ); ogre = monster_paratemp; dragon = monster_paratemp;
Notice how dragon now is equal to ogre because it hijacked the temporary variable. Also note how the parameters send to the receive_damage function are actually holding ogre‘s values. (This might not always be the case though, depending on which order the parameters are copied.)
In order to make this work, we would need to copy the currently active object into another temporary object, move ogre into monster_paratemp like normal, call the function and then revert the whole process.
Hells begins once we start seeing stuff like this:
void monster::chained_attack( monster chained_monster, monster opponent, byte attack_id ){ chained_monster.attack( opponent, attack_id ); }
Conclusion
It seems to me that OOP could very well work out just fine on the NXT using the standard firmware. It might not be exactly like C++, but considering the platform I would say it is quite satisfying.
In my opinion OO is a great way to program and I would love to see it in NXC. (I do realize that it will be quite some work to get it to parse C++ properly, but a man must dream. (Or make his own compiler))
I have only programmed OO for half a year though, so I’m sure there are a lot of features that I don’t know and therefore haven’t considered how to (if possible) to implement.
And again, if you spot any errors, please leave a comment.