📄 chapter4.html
字号:
This incomplete type allows the compiler to perform type checking on a pointer to the structure when the structure is not known. The NEWHANDLE() macro introduces a new type checkable handle into the system.<br><a name="runtypecheck"><br></a><big><b>4.4 Run-Time Type Checking</b></big> <br><br>An important part of any object system is to provide as much error detection and reporting as possible. A common mistake that all C programmers make is accidentally passing an incorrect value to a function. In the case of handles, which are simply memory pointers, passing an incorrect value to a method function may or may not have unpredictable results because the memory pointer may just happen to be valid (but pointing to the wrong memory location). <br><br>Ideally, passing an incorrect pointer to a method function causes some sort of protection fault which would allow you to track down the problem. What if using an incorrect pointer does not cause a fault? The method function more than likely ends up trashing memory instead; in any case, it will not perform the function the caller intended. <br><br>A prime example of how this can happen is using an object handle after the object has been destroyed. The memory pointed to is more than likely still addressable, but there is no valid object in the memory. Another example is memory that has been accidentally overwritten. If the memory contains any object handles, those object handles are now invalid. By far the most common memory overwrite is writing beyond the end of a character array. Detecting this is discussed in <a href="chapter5.html">Chapter 5</a>. <br><br>The first line of defense against incorrect handles is to have the compiler perform type checking on the handles. This makes it almost impossible at compile-time, except through type casting, to pass an incorrect handle to a method function. <br><br>The second line of defense is to have every function that accesses an object verify that the object is an object of the correct type at run-time. In our new object model, the functions that access the object are the method functions, all of which are contained in one source file. Because all the method functions are localized to one file, this opens up interesting optimization possibilities in performing run-time type checking. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> An object system that performs run-time type checking on all objects passed to method functions catches a lot of programming mistakes automatically. </tr></td></table></blockquote><b>4.4.1 Requirements</b> <br><br>Adding run-time type checking into a system is not a new idea nor is it a hard thing to do. However, adding it into a system almost transparently is a challenge. <br><br><i>Low overhead</i>. The first requirement is that run-time type checking must have a minimal impact on the execution time of a program. A 1 percent or less impact would be great. A 10 percent impact would be substantial, but it could be justified. <br><br><i>Fault-tolerant code execution</i>. Any syntax we come up with must be able to support conditional execution of a section of code. If the run-time object verification succeeds, you want the code to execute. If the verification fails, you do not want the code to execute. What good would it be to have a system that notifies you of an object verification failure only to bomb a split second later on code that expected a valid handle but did not have one? <br><br><i>Automatic and easy to use</i>. Any system that we come up with must not require a lot of work on the part of the programmer. The goal is to make the programmer's job easier, not harder. <br><br><i>Minimal code and syntax changes</i>. Again, we do not want to create a system that is hard for the programmer to use. Any system we come up with must not require a lot of code changes. <br><br><i>Does not change the sizeof() an object</i>. A system that changes the sizeof() an object simply because it is being type checked is undesirable and should be avoided. <br><br><i>Must itself not cause crashes</i>. This may seem obvious, but a system must be able to withstand any address passed to it for verification, even addresses that are invalid. It is not enough to simply verify that an object at a valid address is the correct object. It must also make sure that the address itself is valid before attempting to validate the object. This is hard to implement since it requires explicit knowledge of the memory architecture of the hardware you are running on. <br><br><i>Withstands implementation changes</i>. We do not want the run-time type checking to be hard-coded. Instead, it should be isolated through the use of macros. This allows the run-time type-checking implementation to radically change with no source code changes in the modules that use run-time type checking. <br><br><b>4.4.2 What to Use for Data Type Identification</b> <br><br>There appears to be a contradiction in the requirements section. How can type checking be added to an object and not have the object change size? The solution is to let the layer beneath the objects keep track of object types. This layer is the heap manager and, as we found out earlier, the standard heap management routines are not robust enough and would have to be replaced anyway. Why not, then, just add one more argument to the memory allocation routine that indicates the type of the object being allocated? This covers all instances of objects in the class methodology, since all objects are dynamically allocated in the heap. <br><br>The big question now is what to use to indicate the type of an object. We could use a unique integer value, but this is not automatic. It requires the programmer to maintain the list of IDs. Worse yet, in a shared DLL situation, the programmer likely has multiple applications using the same DLL, so what unique integer values are used in this case? Now all applications need to cooperate on the IDs to use. Since this is undesirable, unique integer values will not work. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Using type information, a bad implementation</b>static int nTypeOfHRAND=(hard-coded number);...HRAND RandCreate( int nSeed ){ HRAND hRand=(allocate memory using nTypeOfHRAND); hRand->lRand = nSeed; return (hRand);}</pre></tr></td></table> <br>The goal is to have the object system automatically determine the type identifier of an object. One possible solution would be to have the heap manager generate unique type IDs at run-time as needed. While this would certainly work, there is a much better way. Remember that the entire goal is to come up with any guaranteed unique number as the type identifier. <br><br>Why not just use the address of nTypeOfHRAND as the type identifier! The address is absolutely unique. In fact, you could use the address of anything that is uniquely associated with the class object. <br><br>So, we will create a class descriptor structure that contains information about a class object and use its address as the type identifier. There will be one class descriptor per class. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>A class descriptor</b>typedef struct { LPSTR lpVarName; } CLASSDESC, FAR*LPCLASSDESC;</pre></tr></td></table> <br>Associating the variable name typically used for instances of a class is a valuable piece of information to associate with the class description. This is done with the lpVarName member of the CLASSDESC structure. <br><br>This allows the custom heap manager to produce symbolic dumps of the heap, complete with variable names used in the code. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> An object's type identifier is simply a pointer to its class descriptor structure. </tr></td></table></blockquote>How are these class descriptors going to be named? <br><br><b>4.4.3 Naming Class Descriptors</b> <br><br>The class descriptor address is needed during run-time object verification and it is needed when the object is created. Is there a way that its address can be obtained automatically, without having to specify the actual address by explicit reference? <br><br>Consider the random number generator example. The data type is HRAND and instance variables are named hRand. We want to run-time type check the variable name hRand, not the data type HRAND. Provided the class descriptor name contains hRand, the address of its class descriptor can be determined automatically! How? Through the use of the C preprocessor token pasting operator. <br><br>We now need a macro that, when given a variable name, provides us with the name of its class descriptor. The _CD() macro does this for us. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>The _CD() macro, for use only in other macros</b>#define _CD(hObj) hObj##_ClassDesc</pre></tr></td></table> <br>Notice in _CD() how the token pasting operator (##) is being used in hObj##_ClassDesc to derive the class descriptor name from the object name. <br><br>The _CD() macro begins with a underscore character. This indicates that the macro is to be used only in other macros, not in source code. See <a href="chapter3.html#namingmacros">§3.4.1</a> for more information on naming macros. <blockquote><table bgcolor="#E0E0E0" border=1 cellpadding=2 cellspacing=0><tr><td> The key to providing object-based macros is realizing that the name of an object's class descriptor must be based upon the variable name used in the code and not on the data type of the object. </tr></td></table></blockquote>Basing a class descriptor name on an object's variable name instead of the object's data type is a powerful concept. It allows us to write macros that are object-based. The only piece of information needed is the actual variable name. All other information can be obtained from the class descriptor. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Using _CD() for the hRand object</b>_CD(hRand)<br><b>_CD(hRand) macro expansion</b>hRand_ClassDesc</pre></tr></td></table> <br>In the case of the hRand object, the class descriptor for hRand is named hRand_ClassDesc. <br><br>We are now ready to allocate and initialize the class descriptor. <br><br><b>4.4.4 The CLASS() Macro</b> <br><br>The class descriptor structure used for run-time type checking needs to be allocated and initialized once. There is one class descriptor per class. It makes a lot of sense to do this at the same place in the code where the class structure is being declared. The class descriptor for the random number generator object would look like the following. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>HRAND class descriptor</b>static CLASSDESC _CD(hRand)={"hRand"};</pre></tr></td></table> <br>We can now design a macro that performs all of class descriptor and structure declaration dirty work in one step. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>The CLASS() macro</b>#define CLASS(hObj,Handle) \ static CLASSDESC _CD(hObj)={#hObj}; TYPEDEF struct tag##Handle<br><b>HRAND using CLASS() macro</b>CLASS(hRand, HRAND) { long lRand; };</pre></tr></td></table> <br>The CLASS() macro is used only by source files that implement an object. The CLASS() macro is never used in include files. <br><br>The CLASS() macro takes two arguments. The first argument is the variable name that is used to represent instances of objects of this class. The second argument is the handle name of the class objects. The class descriptor is allocated and initialized based upon the variable name and the stringizing operator (i.e., #hObj). The structure declaration is started based upon the handle name (i.e., TYPEDEF struct tag##Handle). <br><br>Allocating memory for a class object based upon its variable name now becomes incredibly simple. In the case of an hRand variable name, the number of bytes that need to be allocated is sizeof(*hRand) and the type information address is &_CD(hRand). <br><br>We are now ready to implement run-time type checking. <br><br><a name="verify"></a><b>4.4.5 The VERIFY() Macro</b> <br><br>Given any valid variable name that is a handle to an object, an ideal syntax for the run-time object verification macro would be as follows. <br><br><table bgcolor="#CCCCEE" cellpadding=0 cellspacing=0><tr><td><pre><b>Ideal VERIFY() macro syntax</b>VERIFY(hObject);<i>or</i>VERIFY(hObject) { (block of code) }<br><b>VERIFY() and VERIFYZ() macros</b>#define VERIFY(hObj) WinAssert(_VERIFY(hObj))#define VERIFYZ(hObj) if (!(hObj)) {} else VERIFY(hObj)</pre></tr></td></table> <br><br>The VERIFY() macro is designed to be used only by the source file that implements an object, not by other source files that just use
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -