CONCEPT inheritance LAST UPDATE Thu, 2 Dec 1993 05:37:42 +0100 (MET) AUTHOR Hyp DESCRIPTION A step-by-step introduction to inheritance: ------------------------------------------- 1. What does inheritance in LPC mean? Inheritance is a mechanism that allows to reuse existing code, thus saving time, memory and sweat because one does not need to reinvent the wheel all the time. 2. What are programs, classes, instances, objects, etc.? A program is a set of functions and (global) variables defined in a LPC source file. When a program inherits other programs, it appear as if the functions and variables of these programs would have been defined in a single file. Often used in this context are the terms 'parent' and 'child'. A parent is the program that derives its functions and variables to a child program. Instead of program the term 'class' is often used because a program describes the 'behaviour' of objects and therefore it is common to say that all objects belong to a certain class. So when speaking of parent classes it will be refered to programs which are reused by their other programs (child classes). A 'distant parent' is a class which has been inherited indirectly through a 'near parent'. Parent The parent derives its functions & variables to the child. || \/ Child The child inherits the functions & variables from its parent. A class without any parents is often called 'root' and one without any child classes is a 'leaf'. If i.e. you have a file called "/obj/thing.c" then this file defines the class 'thing'. If the class 'weapon' (defined by the file "/obj/weapon.c") inherits the class 'thing' then it becomes a child of the class 'thing'. If there is another class defined by the file "/obj/twohander.c" and inherits class 'weapon' then 'weapon' will be its 'near parent' and class 'thing' would be its 'distant parent'. Classes in LPC are inherited with: inherit "/path/class"; which is the same as: inherit "/path/class.c"; The 'inherit' keyword is not an efun or operator and must not be contained in a function. It is not possible to inherit a class at runtime. Inherited classes have to be specified ahead all function definitions. Good practice is to place the inherit statements at the top of an LPC file. An object is an instance of a class. I.e. if you clone an object from "/std/weapon.c" you create an instance of the class 'weapon'. Each object has its own set of variables which have been defined by its class. But where are the functions? Because they need to be stored somewhere LPC knows of two different types of objects: clones and blueprints. The main difference between a clone and a blueprint is that the blueprint holds (besides its own set of variables) also the functions for its class. An 'abstract' class in object-oriented languages are classes which are not meant to have instances (in LPC it would mean you should not clone objects from it). Such classes serve as a kind of common descriptor for its child classes. In LPC there are no true abstract classes because as soon as a class is used it will implicitly create an object: the blueprint. This behaviour probably changes in newer versions of the driver. But still the idea of abstract classes is used in many mudlibs. I.e. a standard object, defined by "/std/object.c", would hold the functions for setting and querying properties. If now all other classes inherit class 'object' one could assume that all objects then know of properties. Another application for abstract classes in LPC are libraries of functions. Such library classes define often used functions which can be derived to other classes so they don't have to be implemented several times. I.e. the class 'string' defined by "/lib/string.c" would hold lots of functions for string management. NOTE: A blueprint created through inheritance (opposed to cloning from or loading it) does not invoke the initializing function (create() in native mode or reset(0) in compat mode that is). X. What is an inheritance tree/graph? When all classes inherit at most one class then the graphical interpretation of the inheritance structure can be described by a tree: object | thing / \ weapon armour / \ / \ spear axe ring helmet In words: 'object' is the root class and the (distant) parent of all classes. 'axe' is a child of 'weapon' which is a child of 'thing' and so on. 'weapon' is the near parent of spear and 'thing' and 'object' are its distant parents. When classes inherit more than a single class one then inheritance can be described by a graph: object / \ thing room / \ / \ weapon container workroom \ / \ trap npc Here the class 'object' is again the parent of all classes. But now 'container' is a child of 'thing' and 'room', thus it inherits the functions and variables from both classes which i.e. can result in an object that allows to be moved as a thing and to contain objects like a room (but this depends on the implementation). A trap is a weapon and can contain objects (i.e. feed). This may look like an easy way to create new classes to serve as parent for others but the task of designing such a classes is not often as easy as it looks. This is because you have to define the classes in such a way that it allows other classes to reuse their functions but without the need to know everything about their internals. I.e. if a child class uses save_object() and restore_object(), it can effect the parents global variables if they haven't been protected by at least declaring them as 'static'. X. Why not use #include instead of inherit? Including source files is also a way to reuse existing code. But this is a rather primitive form of reuse because it takes place in the preprocessor where the original and the included files are passed as a single one to the compiler. Because the compiler doesn't know anything about included files they will be compiled to a single program. Thus creating large programs which eat up memory. Also you cannot extend functions that are going to be reused by redefining them and calling their ancestor functions. Preprocessor #include's are a form of reuse at file level opposed to inheritance which takes place in the memory of the driver. The only place where including source files makes more sense than using inheritance is when a file becomes too large to be readable. Therefore the file can be splitted into several smaller ones. Those partial files may then be included by a main file which then defines the class. X. How do I access an inherited function? An inherited function can be accessed with the double colon '::' like the following: class::func(); or: "/path/class"::func(); The name of a class is always it's file name without the path prefix. But sometimes it happens that there are two different classes but with the same name. Then one has to use the full file name enclosed in double quotes to specify the right one. It is also possible to omit the class name by just writting ::func(). If the inheriting class does not define its own function called func() one can even omit the double colon. But sometimes this leads to ambiguities. Which function should be called when more than one near parent defines func()? There are two common methods to get around the ambiguity by defining a strategy how to search for inherited functions. The first is called breadth-first and the other deep-first. Breadth-first means to test each of the child's near parent if they have this function defined and if not to step back up to the next level of parents (the near parents of the child's near parents) and check if they do. The search continues till a the function is found or if there aren't any parents left to search in. Deep-first means to search in the first near parent, then in the first near parent of the child's first near parent. If a root class is reached and the function could not be found, then take one step backwards and check the next class and so on. A small example: object / \ thing room \ / container If class 'container' would call ::func() then breadth-first would be to first check class 'thing', then 'room' and then 'object' for the definition of function func(). Deep-first would first check class 'thing', then 'object' and then 'room' if they define such a function. Deep-first is the method used in LPC 3.2 to search for inherited functions. Anyway, try to avoid such situations by specifying which class should be accessed in case of multiple parents. It might even happen, that the method changes from one version to another, so don't rely on it. X. How can I influence the access of functions inherited by other classes? LPC allows to to protect function calls through the so called inheritance modifiers for functions: public func() { ... } The modifier 'public' explicitly marks a function to be accessable by child classes via direct calls and from other objects through a call_other(). It also protects the function from beeing marked differently in child classes. This way one can make sure a function is always accessable no matter which class inherits it. protected func() { ... } If a function is marked as 'protected' then only direct calls from child classes are allowed. A call_other() to it is not possible. private func() { ... } A call to a 'private' function is only possible from within the class itself. Not even child classes can invoke it. Because the function is only known to its own class such a function occupies less memory than the other functions. It is good practice to hide all functions by declaring them as 'private' if they do not need to be derived to child classes. Thus saving memory. When a function is not marked as 'public', 'protected' or 'private' it will be semi public. That means the function will be derived as beeing public but child classes may change it by specifying a prototype for the function with the desired inheritance modifier. A prototype is like a normal function but without an actual body: class1.c: func() { ... } class2.c: inherit "/path/class1"; private func(); This will declare the inherited function func() to be 'private' inside of 'class2'. Now only 'class2' can access it but not the child classes of 'class2' and a call_other() to objects of 'class2' will fail. X. How do I access an inherited variable? An inherited variable cannot be accessed like an inherited function by using the double colon '::'. It is only possible by simply using the name of the variable in the same way it is done when accessing a normal global variable: class1.c: int var; class2.c: inherit "/path/class1"; func() { var += 42; } If 'class2' does not define a global variable , then the one defined by 'class1' will be taken. Because one cannot access inherited variables through the double colon the access to them is doomed to create ambiguities in the case of multiple parents. Here again the driver gets around this problem by defining a search strategy. In LPC 3.2 breadth-first search is used for variables (opposed to deep-first search for functions). A better method to access variables of parent classes is by defining functions to set and query those in the parent class. Then one can use the way of calling inherited functions to access them: class1.c: int var; set_var(int arg) { var = arg; } int query_var() { return var; } class2.c: inherit "/path/class1"; func() { ::set_var(::query_var() + 42); } Doing so is considered to be good practice because you never know how the modification of an inherited variable effects the behaviour of the functions defined by parent. Using such access functions allows the parent class to take care of modifications. I.e. when the parent assumes that its global variables are always set to something not equal 0 (to reduce the amount of if()'s in its code), an attempt to set one of its variables to 0 can be caught by the access function. X. How can I influence the access of variables inherited by other classes? Besides the above described method how to access inherited variables by defining access functions in the parent, variables can be protected in the same way like functions: public int var; protected int var; private int var; ... to be continued ... X. What does redefining a functions mean? When talking of redefining an inherited function, then one refers to a new defintion of a function in a child class: class1.c: func() { ... } class2.c: func() { ... } What makes redefinition of inherited functions so special is that one normally calls the inherited function of the one that has been redefined from inside of it: class1.c: func() { ... } class2.c: func() { ... ::func(); ... } This allows a child class to extend the the functionality of its parent class(es) by redifining functions and adding code around the call of the inherited functions. It is good practice to call the inherited functions first before performing additional actions. You never know how the call to the parent function will affect its behaviour (at least not its internals). Calling it at the end could mean to obsolete the actions prior to the call. I.e.: class1.c: int a, b, c, d; create() { a = b = c = 1; } set_a(int i) { a = i; } class2.c: create() { ::create(); set_a(2); } If the call to ::create() would have come after the call to set_var() then the inherited variable would be 1 instead of 2. X. What is multiple inheritance? Multiple inheritance means a child can have more than just one parent. I.e. if you have a class called 'Rattle' (defined by the file Rattle.c), a class 'Snake' and another class that inherits both you would probably name this class 'RattleSnake'. The overall idea of multiple inheritance is quite easy to understand but not its side effects. Multiple inheritance in LPC is done by using multiple 'inherit' statements: inherit "class1"; ... inherit "classn"; X. What are those side effects of multiple inheritance? What happens when a child's parents have at least one common parent? object / \ thing room \ / container If class 'thing' has a function func() which calls its inherited counterpart object::func() and the class 'room' would have an equal implementation of func(), what will happen when calling func() in the class 'container' if it is defined like this: func() { thing::func(); room::func(); } The functions will be invoked in this order: 1. object::func(); * 2. thing::func(); 3. object::func(); * 4. room::func(); 5. container::func(); The result is that object::func() will be called twice! This often leads to serious problems or at least in a waste of eval cost. I.e. a variable would be incremented twice or two instead of one object would be cloned and so on. How can one get around this problem? Somehow you have to call func() in all parents and allow other classes to inherit them without beeing affected by changes. There are mainly two solutions for this problem. The first is called the 'static method' and the other is the 'dynamic method'. A quick and simple static method is to copy func() from all parents and to create a function that does not need to call thing::func() and room::func() but object::func(). But this kills the idea of reusing existing code and changes in class 'room' and 'thing' would also require modifications of 'container'. Another static method is to split up each func() into two functions where one perfoms just the operations for its own class and the second to call those functions in itself and all its parents: object: _func() { ... } func() { _func(); } thing: _func() { ... } func() { object::_func(); _func(); } room: _func() { ... } func() { object::_func(); _func(); } container: _func() { ... } func() { object::_func(); thing::_func(); room::_func(); _func(); } If we now call container::func() all parents will perform their operations only once. The disadvantages are, that it doubles the amount of functions leading to a higher memory usage and class 'container' needs to know all about its parent classes and their dependencies. The dynamic method is to add extra code to perform runtime checks if a function allready has been called through inheritance or not. A detailed description of this method will be omitted because it would simply lead to far off the topic and there are plenty different ways how it could be done. The disadvantages here are that extra code is neccesary at the cost of additional memory, code becomes unreadable and decreases the performance of the function calls.