[ CnUnix ] in KIDS 글 쓴 이(By): Convex ( Hull) 날 짜 (Date): 1993년10월16일(토) 12시07분53초 KST 제 목(Title): c++ critique From ian@syacus.acus.oz.au Wed May 13 23:12:20 1992 Sure. Here is the text. You can also ftp the postscript as follows: Indiana University have kindly offered to make the critique available for ftp from: Machine: iuvax.cs.indiana.edu IP # 129.79.254.192 Directory: pub Compressed: cpp.crit.ps.Z (preferred) Uncompressed: cpp.crit.ps (Only if you can't uncompress) Hours: After 6pm Eastern US Please Please observe the after hours embargo, as they have offered to do this completely voluntarily. Please report any problems to ian@syacus.acus.oz.au. Thank you. -- Ian Joyner ACSNet: ian@syacus.acus.oz ACUS (Australian Centre for Unisys Software) Internet: ian@syacus.acus.oz.au 115-117 Wicks Rd, North Ryde, N.S.W, Australia 2113. Tel 61-2-390 1328 Fax 61-2-390 1391 UUCP: ...uunet!munnari!syacus.oz Disclaimer:Opinions and comments are personal. A C++ Critique by Ian Joyner Note: This is a long submission. There is also a postscript version available on request. Any reasonable comments appreciated. Introduction1 C++ Specific Criticisms4 Virtual Functions4 The Nature of Inheritance7 Name Overloading8 Function Overloading9 A Note on Overloading and Nesting11 A Note on "A Discipline of Programming"14 Pure Virtual Functions15 Virtual Classes15 '::"' '.' and'->'16 On information, computation and execution and paradigms17 Anonymous Parameters in Class Definitions19 Constructors21 Constructors and Templates21 Optional Parameters22 Bad Deletions23 Local Entity Declaration23 Friends25 Static26 Union27 Nested Classes28 Global Environments29 Header Files29 Class Header Declarations31 Calls on references to deallocated objects (Dangling Pointers)32 Type-safe Linkage33 C++ and the Software Lifecycle34 Reusability and Communication35 Reusability is a matter of Trust35 Concurrent Programming36 The Role of Language37 Generic C Criticisms40 Pointers40 Arrays41 Function Parameters42 void *42 void fn ()43 fn ()43 ++, --45 Defines46 Case Distinction46 Assignment Operator48 Type Casting49 Semicolons50 Conclusions52 Introduction ============ This is a critique of the C++ language. It is an attempt to address the questions of whether C++ is a good OO language, can it be used to easily implement quick and small projects, how well does it scale up for large projects, and technical questions of how far it goes in supporting or hindering general good programming practices, and as a result the production of quality software? What is the relationship between a language and compiler, and software developers; and between the language and compiler, and the target system? A programming language functions at many different levels and roles, and must be critiqued with respect to those levels and roles. Not only should a language be critiqued from a technical point of view, considering its syntactic and semantic features, but it should be critiqued from the viewpoint of its contribution to the entire software production process, how it facilitates communication between levels, and how the communication of ideas may be facilitated from one programmer to another, as often one programmer may not be responsible for a whole task over its lifetime. The primary purpose of language is to facilitate communication, and a programming language must facilitate the consistent description of a system which satisfies the requirements of the problem at hand. It should be able to provide communication between programmers perhaps separated in space and time. A language should be able to express the intent of the systems designers and implementers. A language should also be able to provide mechanisms for project tracking, to ensure that modules (classes and functionality) are completed in a timely and economic fashion, and that the modules produced satisfy the original requirements. A programming language aids reasoning about the design, implementation, extension, correction, and optimisation of a system. The preface to "The C++ Programming Language" has an interesting quote - "Language shapes the way we think, and determines what we can think about." - B.L.Whorf. Does language shape the way we think? Does C++ shape the way we reason about programming? What is the role of language and what is the role of a computer language? Fundamental to the reusability of software is that subsequent programmers may be able to understand the nature of that software, by means of the communication medium and descriptive abilities of the programming language. Programming is a dynamic activity. Program text usually does not stay static for long, especially during the development stages. Languages and compilers should provide consistency checks that aid in this dynamic process, ensuring that as program text is added or modified, previous work is not invalidated. That a compiler may be able to translate the program into an executable entity is a secondary, and yet not unimportant consideration. A compiler has a twofold role. Firstly, to generate code that the target machine may execute, but secondly, and more importantly, to check that the programmers expression of the system is indeed a consistent expression. There is certainly not much point in generating code for a system, where parts of the description of that system has automatically detectable inconsistencies. The language definition provides the framework that makes this role of the compiler possible. The task of the machine is to execute the produced programs. Programming languages should not be the exclusive domain of programmers, but should be tools in which design, and even requirements analysis may be achieved. Object-oriented techniques emphasise the importance of this, as abstract definition, and concrete implementation are separated, yet provided by the same syntax. Program text is the only reliable documentation of the system, so therefore, the language should explicitly support the task of system documentation. As with all language, the effectiveness of communication is dependent upon the skill of the writer. Good program writers require languages which support the role of documentation. They also require that the syntax of languages is perspicacious, and easy to learn, so that those not necessarily trained in the skill of 'writing' programs, may be able to read them, and gain understanding of the system, just as those who enjoy reading novels are not necessarily novelists. Can grafting OO concepts onto a conventional language realise the full benefits of OO? Perhaps a biblical quote can be applied: "No one sews a patch of unshrunk cloth on to an old garment; if he does, the patch tears away from it, the new from the old, and leaves a bigger hole. No one puts new wine into old wineskins; if he does, the wine will burst the skins, and then wine and skins are both lost. New wine goes into fresh skins." Mark 2:22 There is certainly a rush to the concepts of OO. OO has been a rallying point to which good software production practices have been gathered. These tended to have been lost in the structured programming world, as it lost its original focus (which was really object-oriented anyway). Now that OO has gained critical mass, it too is in danger of loosing its focus. OO extensions have been proposed for COBOL. One could speculate that there will be an OO BASIC. How useful is the extension old technology languages to support some OO concepts, unless there is a wide scale abandonment of the concepts and practices in those languages that are contrary to the principles of OOP? How well can such hybrid languages be expected to support the sophisticated requirements of modern software production? Surely a basic premise of object-oriented programming is to enable the development of sophisticated software by the adoption of the simplest techniques possible, and that the software development techniques and methodologies do not impede the production of such systems? All languages are affected by compromise and tradeoffs to some extent. To what extent do the decisions made affect the consistency of the concepts within the language, and the ease with which those concepts can be applied to any given problem? A language is not just syntactic constructs in isolation, but is the effects of the combination of all constructs. It is widely accepted that a good user interface will not give the user the option of choosing illegal actions, or combinations of actions. A poor user interface allows the user to do such things, perhaps correcting the situation with annoying error messages, or worse to execute an action that was not intended. To avoid error is better than to fix, (most people drive their cars with this principle in mind.) End users of computers understand such measures of quality, but programmers commonly accept analogous pit-falls in the languages and tools they use. Does the extra complexity disguise such problems, or preclude simple and consistent solutions? A critique of C++ cannot be separated from criticism of the C base language, as it is essential for the C++ programmer to be familiar with C, and many of C's problems affect the way that object-orientation is implemented and used in C++. This critique may not be exhaustive of the weaknesses of C++, but it attempts to illustrate the consequences of these with respect to the timely and economic production of quality software. The format of this report is that it is structured around technical considerations, with explanations and illustrations of how they affect the overall goals of software production and object- oriented programming. Firstly, it looks at criticisms that may be regarded as specific to C++, then at problems more general to C, and finally summarises with some conclusions. The critique makes two general types of criticism. Firstly, there are safety concerns. These concerns affect the end user perception of the quality of the program. They can result in program crashes, or failure to meet requirements. Compilers can check for potential crashes, but they can also check for inconsistencies that may lead to failure to meet requirements. Languages provide the framework by which such compiler checking is made possible. Often this checking is enabled by requiring the specification of redundant information. Declarations themselves are an example of such redundancy. There is much redundancy in the source form of a program. The compiler uses this information to perform consistency checks, and then throws this information away, and produces as a result the executable system. There is a misconception that safety checks are 'training wheels' for student programmers. This is quite an immature conception of programming. Languages such as Pascal were widely adopted by universities, because they were good languages. The best programmers realise that we are not good programmers, and as a whole, the computing community is still learning to program. Secondly, there are 'courtesy' concerns. These concerns affect the view of quality of the program from within the development and maintenance processes. A fuller explanation of courtesy concerns is given later. Courtesy concerns are generally of a syntactic nature, whereas safety concerns are generally of a semantic nature. C++ Specific Criticisms ======================= Virtual Functions ----------------- Polymorphism and function name overloading are key concepts of OOP. A language designers choice is whether this should be specified in the parent or the inheriting class, in other words, is it the decision of the designer of the parent class, or the descendant class? There are cases, which can be made for both. Indeed both are not mutually exclusive, and can be catered for quite easily in an object-oriented language. There are three options: 1) the redefinition of a routine is prohibited, descendant classes must use the routine as is; 2) a routine may or may not be redefined, descendant classes may use the routine as provided, or may provide their own implementation, as long as it conforms to the original interface definition, and accomplishes at least as much; 3) a routine is abstract, and not defined, no implementation is provided, and non-abstract descendent classes must provide a definition. Cases 1 and 3 must naturally be decided by the parent class designer, and the language should provide appropriate syntax for these cases. Case 2 is the most common case and is the decision of the descendant class. Option 1 In C++, the first case is not catered for. The closest is to not specify the routine as virtual. However, this means that the routine may be completely replaced, but is not virtual. This causes two problems. Firstly, it means that the routine may be unintentionally replaced in a descendent. This error should be averted by the compiler reporting a syntax error due to 'duplicate declaration'. This is logical, as descendant classes actually are part of the same name space as the classes that they inherit from, and the redeclaration of a name should cause a name clash within that scope. (See the section on name overloading for further explanations.) The second problem is illustrated by the following example: class A { public: void nonvirt (); virtual void virt (); } class B : public A { public: void nonvirt (); void virt (); } A a; B b; A *ap = &b; ap->nonvirt (); // calls A::nonvirt, even though this object is of // type B. ap->virt (); // calls B::virt, the correct version of the routine // for B objects. In this example, class B has presumably replaced, or added to the functionality of routines in class A for all objects in the subset class B. This example shows how C++'s virtual mechanism breaks a kind of safety. (This is like type safety as a routine not intended for objects of type B has been applied to an object of type B. But as objects of type B are also of type A, and A::nonvirt has been written for objects of type A, this cannot be strictly labeled type safety. This example shows that there is more to consistency and safety than can just be termed 'type' safety.) It could be argued that the C programmer knows what was intended, and this is how the language is defined, so there is no problem. If there was no other easy way to achieve the same effect then undoubtedly there could be no criticism. However, if the intention is that sometimes A::nonvirt and sometimes B::nonvirt be applied to objects of type B, the solution is simple, A::nonvirt and B::nonvirt must have different names. This solution means that all erroneous applications of the above construct will be caught. That way the compiler can be trusted to detect such potentially inconsistent situations. The problem therefore lies with the language definition. C++'s non-virtual overloading mechanism precludes this set of potential errors from being detected. A further argument is that any statement should consistently have the same semantics. A typical object-oriented interpretation of a statement like a->f () is that the most suited implementation of f() is invoked for the object referred to by 'a'. In the above example, the programmer must know whether the function f() is defined virtual or non-virtual in order to interpret exactly what a->f () means, and therefore, the statement a->f () is not implementation independent. A change in the declaration of f () will change the semantics of the invocation. Implementation independence means that a change in the declaration or implementation DOES NOT change the semantics, of executable statements. If a change in the declaration does change the semantics, then this should generate a compiler detected error, and the programmer must then be required to make the statement semantically consistent with the changed declaration. This reflects the dynamic nature of software development, where the program text is not static, but is subject to change, and therefore consistency checks are a necessary part of the role of a compiler. For yet another case of inconsistent semantics of the statement a->f () vs constructors, section 10.9c, p 232 of the C++ ARM should be consulted. Option 2 The second case should be left open in the parent class, and the decision left for the programmer of the descendant class, but C++ generally requires (although virtual can be introduced in an intermediate class in the inheritance chain) this decision to be made in the parent class, by the specification of virtual. This may be a problem in that it means that routines which aren't actually redefined in a descendant class are accessed via the slightly less efficient virtual table technique, rather than a straight procedure call. (Although this is never a large overhead, but object-oriented programs tend to use more and smaller routines, meaning that routine invocation becomes more of a significant overhead.) An optimising compiler and linker may replace routine invocations which do not in reality require the virtual mechanism by straight procedure calls. The policy should be that any routines which may potentially be redefined should be declared virtual. A potentially more serious problem is that since the programmer of the descendant class does not specify that the routine is being intentionally redefined, the original routine may be redefined unwittingly. Object-orientation means that the same name can be used, but the programmer should be conscious that this is what they are doing, and state this explicitly. If the programmer is not aware of this then this should be a syntax error reporting that the name has already been defined. C++ adopted the original approach of Simula, and this has been improved upon, and other languages have adopted better, more explicit approaches, which avoid the error of mistaken redefinition. Eiffel and Object Pascal cater for this situation by requiring the descendant class programmer to specify in that class, that redefinition is intended. This has the extra benefit that a later reader or maintainer of the class will be able to easily identify which routines have been redefined, and that this definition is related to a definition in an ancestor class without having to refer to ancestor class definitions. Another problem with the C++ scheme is the ability to reuse function names based on different function signatures. (See name overloading for a further and alternative explanation). Option 3 The third case is catered for by the pure virtual function, which means the routine is undefined, and the class is abstract and cannot be directly instantiated. If a descendant class is to be instantiated, then it must define the routine. Any descendants that do not define the routine, are also abstract classes. There is nothing wrong with this in concept, but see the section on pure virtual functions for a criticism of the syntax. Virtual is not an easy notion to grasp. The related concepts of polymorphism, redefinition, and overloading are easier to grasp than virtual, being oriented towards the problem domain. Virtual routines are the underlying mechanism by which redefinition in heirs is implemented. Virtual is an example of where C++ obscures the concepts of OOP as the programmer has to come to terms with the low level concepts, rather than the higher level object-oriented concepts. Interesting as underlying mechanisms may be for the theoretician, or compiler implementer, the practitioner should not be required to understand or use them to make sense of the higher level concepts. Having to use them in practice is tedious and error-prone, and may prevent the adaptation of software to further advances in the underlying technology and execution mechanisms (see concurrency). In summary: the concepts which should be expressed are, that a parent class should be able to prevent redefinition of selected routines, but in general, all other routines should be left open for redefinition, as prevention of redefinition is the exception, rather than the norm. Explicit syntax to provide the three options makes the programmers intent clear, and can avoid rare, but albeit subtle and possibly disastrous errors. To avoid mistaken name overloading, the redefinition of a routine should be explicitly stated in the descendant class. C++'s virtual mechanism may be used in such a way that a kind of safety similar to type safety, but not strictly type safety, is broken. A routine may be applied to an object of a class for which it was not intended. A compiler may determine exactly which routines need to be made or not made virtual. The Nature of Inheritance ------------------------- Inheritance is by nature a close relationship. Objects that are instances of a class are by definition also instances of all ancestors of that class. In order for there to be effective object- oriented design, the consistency of this relationship must be preserved. Consistency can be checked by ensuring that each redefinition in a subclass is consistent with the corresponding definition in the ancestor class. This means that the original design and requirements of the ancestor class continue to be met through successive levels of refinement. If the requirements cannot be met, then this indicates an error in the design or implementation, and indicates that rework is required. Thus the consistency due to inheritance is fundamental to object-oriented design. C++'s implementation of non-virtual overloading, and overloading by signature (see below) means that the compiler cannot check for this consistency. Therefore C++ does not realise effective object-oriented design. Name overloading ---------------- The choice of names is of fundamental importance in the production of self-documenting and understandable, and therefore reusable and maintainable software. Name overloading is a powerful, but problematic technique. The problem with it is that two entities with the same name cause confusion. This is easily illustrated by considering a group of people, where two or more have the same first name. This problem has been overcome in conventional languages by providing the notion of scope, where the same name can be used in different contexts. For example, brothers in the one family, are usually assigned unique names by their parents. When a name is used twice within the same context it should be a syntax error, due to duplicate definition. In object-oriented programming, the scope of a name is the class in which it occurs. If a name occurs twice in a class, it is a syntax error. Inheritance introduces some questions over and above this simple consideration of scope. What should happen to the scope of a declaration in a base class, in a derived class? There are three choices. Firstly, the name is in scope only in the immediate class where it is declared, but not the subclasses. Secondly, the name is in scope in a subclass, but the name may be reused, whether the declaration is virtual or not. Thirdly, unless the name is reused in the context of reimplementation of a virtual routine, the reuse of the name is reported as a syntax error. This relationship of name scope is obviously not symmetric, as names in a subclass will not be in scope in a superclass (or references to objects of the superclass, although this is not the case in typeless languages such as Smalltalk.) The first case precludes software reusability, as subclasses will not inherit definitions of implementation, so case 1 is not worth considering. The second case is C++'s approach. There are two potential problems which may arise. Firstly, the name may unintentionally be reused, as the compiler does nothing to stop this. Secondly, the parameters being passed to the routine cannot be type checked against the original routine, as there is not assumed to be any relationship between the original, and the new routine. (See the note on name overloading and nesting for a further explanation of the problems of scoping vs inheritance). Since consistency checks between the superclass and subclass are not possible, the tight relationship that is implied by inheritance, which is fundamental to the design of object-oriented systems, is not guaranteed. This can possibly lead to inconsistencies between the abstract definition of a base class, and the implementation of a derived class. If the derived class does not conform to the base class in this way, it should be questioned why the derived class is inheriting from the base class in the first place. (See the nature of inheritance.) In order to provide the consistent customisation of reusable software components, object-oriented programming permits the same name to be used, only by the redefinition of the original entity. The programmer of the descendant class should tell the compiler that this is not a syntax error due to a duplicate name, but that redefinition is intended. (This has already been covered in the virtual section.) In object-oriented programming, two entities with the same name may be used in the same context. This conflict must be resolved by qualifying the names, so that the correct entity is referenced. Names must be qualified with the object to which they belong. Thus two entities named 'field' may be distinguished by 'a.field', and 'b.field'. This is a bit like in a group of people with the same name, qualifying which person is intended by adding their family name. In summary: The relationship of inheritance implies a tight relationship between base and derived classes. If consistency checks are not satisfied, then this is a good indication that the design of the system is incorrect, and detection of such faulty designs at an early stage will save much wasted work. C++ fails to provide an important class of consistency checks. Function Overloading -------------------- C++ allows functions to be overloaded if the arguments in the signature are of different types. This has examples where such overloading can be quite useful, for example: max (int, int); max (real, real); will ensure that the best routine for the types int and real will be invoked. However, object-oriented programming actually provides a variant on this. Since the object is passed to the routine as a hidden parameter ('this' in C++), an equivalent but more restricted form is already implicitly included in object- oriented concepts. A simple example such as the above would be expressed as: int i, j; real r, s; i.max (j); r.max (s); but i.max (r) and r.max (j) will give syntax errors, as the types of the arguments do not agree. (By operator overloading of course, these may be better expressed, i max j and r max s, but min and max are peculiar functions that may want to accept two or more parameters of the same type.) Now the above shows that in most cases, function overloading can be expressed consistently within the object-oriented paradigm, without the need for the function overloading of C++. However, C++ makes the notion more general, as the function may be overloaded by more than one parameter, not just the implicit current object parameter. The generalisation of any technique is often a good thing, as it removes restrictions. In this case it may provide a useful mechanism, but how many examples require this flexibility, and how could they be programmed in other ways? However, in order to provide such flexibility introduces several potential errors which the compiler will not be able to check, as it must assume that what may be inconsistencies, are correct, in order to allow such overloading to be provided. If the programmer intends to redefine a virtual routine, but makes a mistake in the declaration of the function signature, then an entirely different function will be assumed to be defined by the compiler, and any calls to the function using one or other of the signatures will also fail to detect the inconsistency. If the programmer does make a mistake in supplying the parameters on a call to the routine, a C++ compiler will not be able to be specific about the error, all it can report is that no function with a matching signature could be found. Presumably, if the programmer has made this sort of mistake it is for subtle reasons, and may be difficult to actually pinpoint which parameter was at fault. Secondly, the incorrect parameter may accidentally match, one of the other routines. In which case this error will be propagated into the production code, and may not be tracked down for a long time. If it is felt though that C++'s scheme of having parameters of different types is useful though, it should be considered that object-oriented programming does provide this but in a more restricted and disciplined form. This is done by specifying the parameter as needing to conform to a base class, and then any parameter passed to the routine may be a type of the base class itself, or any class derived from the base class. For example: A.f (B someB) {...}; class B ...; class D : public B ... A a; D d; a.f (d); This means that strict type checking of parameters is possible, as the entity 'd' must conform to the class 'B'. The alternative to function overloading by signature, is to require all names to be unique. Names should be the basis of distinction of entities, and is the purpose of naming. This is known to work, solves the above problems, as the compiler can cross check that the parameters supplied are correct for the given routine name, and furthermore will result in better self-documented software. Often it is difficult to choose appropriate names for entities, but this is well worth the effort. Philosophically, a name is most important in describing the characteristics of a person or entity. Names give things their identity. In summary: The relationship of inheritance is a strong relationship. It is the basis of reusability, and the ability to be able to assemble software systems out of preexisting components. Such assembly requires that consistency between the components being assembled must be checked. Assembling a jig-saw requires that not only the pieces fit in their cut out shape, but that the resultant picture makes sense. Assembling software components is more difficult, as unlike the jig-saw, which is reassembling that which was complete before, assembling software components is building a system that has never existed before. C++ has not implemented the notion of inheritance in a manner that fully exploits the advantages of inheritance, with regards to software reusability. This may lead to the opinion that C++ is not a truly object-oriented language, as it does not ensure that programs written in the paradigm are consistent with the paradigm. Certainly, there are not very many reusable C++ libraries available, which tends to suggest that C++ may not support reusability as well as possible. This means that one of the fundamental goals of object-oriented programming has not been met. A note on name overloading and nesting -------------------------------------- Name overloading should be provided and used in ways that are explicit and expected. Object-oriented programming provides name overloading by inheritance and redefinition. Routines provide name overloading, by local declarations of names being limited in scope to the routine of declaration. Thus the same name may be used in other routines, without any clash in the name space. This provides an important benefit, the prevention of name clashes, and enables two programmers to independently write routines without being concerned that the other programmer is using the same name, so that the modules may later be combined without the programmers having to worry about name clashes. Nesting of routines, or blocks has always had a problem with overloading, in that if a name is redefined in an inner block, then it may mistakenly override the definition of the name inherited from an outer block, and the inner block may not access the entity of the outer block, unless some extraneous entity qualification mechanism is introduced into the language (which most languages chose not to provide.) This report suggests that restriction to only these two mechanisms (redefinition in subclasses, and local scope to routines) is necessary to gain the maximum benefit from name overloading. This is reasoned by common sense, but the proof of this is in the realm of the language theorist, and beyond the scope of this document. The exclusion of all other forms of name overloading results in no loss of generality, and only the slight inconvenience that the programmer has to choose unique names, as the compiler will report name clashes as a syntax error due to 'duplicate identifier'. Note this is only a potential run time error, if this level of safety is not provided. An automated tool such as a compiler has no way of guessing the programmers intent, and therefore is bound to report this as an error, as it could be an inconsistency, not because it is an inconsistency. The use of name overloading in such forms is frequently found to be the cause of subtle errors. It is also suggested that contrary to the usage in most high level languages, that a name should not be overloaded, while it is in scope, and that the reuse of a name in nested procedures and blocks should result in a syntax error. In fact textually nested blocks 'inherit' the named entities of outer blocks, and perhaps as argued for virtual, explicit redefinition of names should perhaps be provided to avoid error. However, this actually provides no or very little benefit, unlike redefinition of inherited entities in the class sense. Nesting automatically makes entities tightly coupled. Consider another possible error that can happen due to nesting. If an overloaded nested declaration is removed, then references to that name will not result in syntax errors due to their being the same name in the outer environment. An instruction mistakenly not removed that changes the value of the entity, will now mistakenly change the outer entity. Consider: { int i; { int i; i = 13; } } Now delete the inner declaration: { int i; { i = 13; // Syntactically valid, but not the intention. } } Such nested blocks as above are also contrary to the principles of object-oriented programming, as inner blocks are 'tightly' coupled to outer blocks, and inner blocks have access to the entire environment of the outer block, instead of through well defined interface routines. Nested blocks are the opposite of the loosely coupled software components, which are the ideal of object-oriented programming. Nested blocks are similar to parent and child classes as a child inherits entities defined in a parent, and an inner block inherits entities defined in an outer block. This is why textual nesting is not required in object-oriented languages, as the 'nesting' mechanism of inheritance is more sophisticated. The above example also shows that a program text is not a static entity, but a dynamic one. Programmers may easily make modifications to program text that violates the consistency of the original system. This scenario happens often, as a programmer may come back to program segments written some time before, so that some or many of the original details are forgotten, or modifications may have to be made by a completely different programmer. These scenarios are very common today, especially with large projects, and with software that evolves over time to meet new and changing customer requirements. In this case it is vital that the language and compiler system be trusted to detect any possible inconsistencies that may be introduced into the system due to modifications. The restriction of concepts to their useful forms, is only a restriction on the language, and coincidentally have the effect of keeping the language simple. Such restrictions do not in general restrict the programmer in the production of flexible, and efficient software, but aid in the production of such software, by avoiding subtle errors which may otherwise occur. Such restrictions in the language and compiler in fact aid flexibility in the software process, as programmers can more confidently make changes, and rely on the compiler to make sure that the original programmers intent has not been contravened. The most useful tools in any trade are those that do exactly what is needed and nothing more. Name overloading is just such a tool. It is very useful, but where it is provided in more than its most useful forms, the less useful forms 'blunt' the effectiveness, and even work against the usefulness of overloading, as confusion of names becomes greater, meaning that name clashes and inconsistencies of use cannot be detected. Taken to an extreme, name overloading may mean that one name can be used for every entity. Then we may as well do away with naming entities altogether. After all, names are not needed in order to program, the compiler just throws them away in the resultant code. In summary: Name overloading is good. Name overloading is bad. A Note on "A Discipline of Programming" --------------------------------------- Having written the previous section, 'An essay on the Notion: "The Scope of Variables"', in E.W. Djikstra's "A Discipline of Programming", was revisited, to find very similar reasoning about declarations in nested situations. The following points come from his argument. Programming techniques facilitate the management of extremely large state spaces. There are two notions here, of how to manage the state space, and how to manage change within the state space. This is precisely the activity of programming. State spaces may be managed by division, and nomenclature (naming entities), within those divisions. The activity of division must cleanly divide the state space into independent entities, and nomenclature must provide a consistent system, where name clashes, interference, and confusion are not possible. A scheme that implements this will necessarily be as simple as possible. State change within a state space, must also be provided in the simplest possible manner, and one such mechanism is available, and has been used successfully in many programming languages, the assignment, which changes the values of variable entities within a state space. These notions are the basis of keeping programming a simple and manageable activity. The larger state spaces (systems and programs) grow, the more necessary it is to observe the simplicity of software organisation. It is the purpose of programming languages to implement these concepts in the simplest, and therefore the most manageable forms. Pure Virtual Functions ---------------------- As mentioned above, pure virtual functions provide a means of leaving a function undefined and abstract, which also means, that a class with such an abstract function may not be directly instantiated, as the function must be defined in a descendant class. The concept is quite alright, but the syntax to express this concept illustrates where C++ misuses mathematical notation to express a natural concept. The syntax is - virtual void fn () = 0; This syntax on the face of it leaves the reader to guess as to what its meaning is, even those well versed in object-oriented concepts. The choice of designing this construct is to use some indirect mathematical construct as C++ has done, or to use a keyword, which directly expresses the concept. Direct expression of concepts enhances communication, and the ease with which a language may be learnt. The C++ decision is in keeping with the C philosophy of avoiding keywords, but often this avoidance is at the expense of clarity. This concept would have been more clearly implemented by a keyword, such as - pure virtual void fn (); or abstract void fn (); A suggestion that the mathematical notation makes is that values other than zero may be used. What if 13 is equated to the function? - virtual void fn () = 13; A simple suggestion to fix this is to define '= 0' as abstract: #define abstract = 0 then virtual void fn () abstract; Virtual Classes --------------- If class D multiply inherits class A from classes B and C, then if it only wants to inherit a single copy of class A, the inheritance of A must be specified as virtual in both B and C. Two questions arise from this. Firstly what happens if A is only declared virtual in only one of B or C, and secondly, supposing a class E wants to inherit A via B and C by a different policy, ie multiple copies of A, than in D. In C++, the virtual decision must be made early, reducing the flexibility which may be required in the assembly of derived classes. In a shared software environment, as classes B and C may be provided by different suppliers, it should be left to the implementer of class D or E, exactly how to resolve this problem. Such flexibility is key to reusable software. In principle you cannot envisage when designing a base class all the possible uses in derived classes, and the attempt to do so considerably complicates design. C is supposed to trust programmers, and in unimportant ways gives them flexibility, but the design of C++ restricts flexibility where it actually is important, especially in the object-oriented sense. '::', '.' and '->' ------------------- While the '.' and '->' syntax came from C structures, it is heavily used in C++ in accessing class members, and illustrates where the C base adversely affects the flexibility required of an object-oriented language. When the programmer declares an entity, a decision is made about how the entity will be accessed. The declaration decides whether the entity is accessed directly, or via a pointer. When members of the entity are accessed, the programmer should not be required to know how exactly to access them, but should access them in an implementation independent fashion, without the need to know whether the entity is being accessed directly, or via a pointer. This is because the compiler can determine exactly how to access the entity. In more detail, C++ x.y means access the member y directly in the structure or object x, whereas x->y (or the equivalent *x.y) means access the member y via the pointer x. One of the key concepts of object-oriented programming is that access to entities is independent of implementation. The realisation of implementation independence is a simple matter for compiler technology, as the compiler knows from the declaration of 'x', whether x is an object itself, or a pointer to an object. The compiler may therefore be relied upon to generate the correct access code. Thus only one access operator is required in a high level language, not both '->' or '.'. This illustrates why declarations are important for implementation independence, and why type safety is also important. A further problem is that the distinction reduces flexibility in the software fabrication process. If it is required to change an objects status from via a pointer to direct, all '->' dereferences must be changed to simple '.'. This discourages the optimisation of changing an entity to direct access, although the correction of changing a direct access to access via a pointer may be forced. For declarations, C++ requires the use of the '::' form. Most languages use one syntax (typically the dot) for both declarations and access. '::' is also defined as the scope resolution operator. (Interestingly, in an early paper, 'Classes: An Abstract Data Type Facility for the C Language', 1981 by Bjarne Stroustrup, '.' was used instead of '::'.) Scope resolution is a difficult problem with regards to the inheritance of features from a common ancestor due to multiple inheritance. It would be more advantageous if such accesses were provided by one consistent syntax. (Scope resolution by the '::' operator is rather like qualification by 'qua' in Simula.) It is better to make decisions of this nature in declarations, and rely on the compiler to generate the different code for different accesses, as if the declaration policy ever changes, then all that is required is one recompilation, rather than any rework of the program text by the programmer. In summary: access to members should be provided by the same syntax, as this ensures implementation independence, and thereby enhances programming flexibility. A change in access policy may then be effected by the change in the declaration only, and simple recompilation. This is enabled by compiler technology, which means the compiler can generate the correct code for the given semantics, therefore relieving this burden from the programmer. On information, computation and execution and paradigms ------------------------------------------------------- The role of programming may be seen as the separation of concerns. This separates information modeling from the specifics of computation, and thereby, computation from the specifics of execution. This model of programming ensures data independence, and therefore flexibility and portability in software production, and the above argument, and others in this paper are a consequence of this. The information model, can be seen as setting up policies of data form, and type, and the interfaces and operations on this data. The computation model can be seen as specifics of the computations that can take place on the information. The execution mechanism is a combination of compilers and the target architecture and environment. The execution mechanism is responsible for the combination of policies from the computation domain, and the information domain, into an executable system. For example, if the computation 'a + b' is to be executed by the execution mechanism, the execution mechanism must determine whether a and b are integers or reals, and actually invoke the correct mechanisms to execute the add based on those types. Similarly, if 'a.b' is specified in the computation domain, the execution mechanism must look up the access policy on a, in the information domain. If a is an object, then 'a.b' is executed in C terms. If a is a pointer to an object, then 'a->b' is executed in C terms. In a compiled system, these decisions are mode early, at compile time, so there is no run-time overhead. In an interpreted system, these decisions are made at run-time resulting in some overhead, but with the advantage of extra flexibility. A schematic of the information, computation and execution mechanism looks like: ---------------------- ----------------------- | | | | | Information | | Computation | | Model | | Model | | | | | ---------------------- ----------------------- \ / Information \ / Computation Policies \ / Policies \ / V V ------------------------------------- | | | Execution Mechanism | | (Compiler + target architecture) | | | ------------------------------------- The information and computation policies are determined by requirements analysis, design, and implementation. This separation of concerns, makes the information, and computation aspects of a program independent, and this ensures flexibility. Interestingly, ISO's Reference Model for Open Distributed Processing describes a similar model in order to ensure distribution transparency of applications and services within a network. Another way to look at this is that the information model provides a schema, by which the computational model is interpreted to be meaningful, and to which the computational model must conform. It is the compilers job to ensure that the computational model conforms to the information model. In natural language, we use schemas frequently. For example, the sentence "The boy drank the computer and switched on the glass of water" is grammatically correct, but the computational model of verbs does not conform to the information picture that we understand about computers and glasses of water. Now programming has much more precisely defined semantics than natural language, so naturally we should expect to find many more such schema checks within programming languages. There is another concept in object-oriented programming which is closely related, that of paradigm. Object-orientation is a paradigm. If the object-oriented paradigm is strictly implemented, there are also many consistency checks that a compiler may apply, similar to those of schema checking. Between the dual concepts of schema and paradigm, a solid and disciplined framework for program development is provided. Multi-paradigmed approaches have also been proposed, to bring the advantages of several paradigms into one framework. But multi- paradigmed approaches have their own problems, and haphazard mixing of paradigms can lead to inconsistencies between the paradigms. Indeed if two or more paradigms are badly mixed, we may end up with something of little advantage. At best multi-paradigmed technology is immature. To get any advantage of a multi- paradigmed approach, paradigms will have to be carefully combined, and not necessarily equally. It has been suggested that C++ can be used for many different styles or 'idioms' of programming. This is very similar in concept to multi-paradigmed programming, but it has been recognised that bending languages to resemble other languages by defines and other techniques is problematic, and the Swiss Army knife approach cannot be as efficacious as a tool which is specifically designed for the purpose of application. By this approach, syntactic constructs similar to those used in the paradigm may be built, but in general checks to make sure that the constructs are used consistently with the paradigm are impossible. One of the strengths of the object-oriented paradigm is that it is excellent for the large scale organisation of software. Therefore, the object-oriented approach may be most useful for providing the overall structure for systems written in other paradigms. The object-oriented paradigm may provide the framework, within which other paradigms, such as logic, procedure, functional, and machine oriented paradigms can exist. Thus an object-oriented framework will provide an object-oriented wrapping for embedding other paradigms. One example is Apple's MacApp, which provided an object-oriented wrapping around the Macintosh Toolbox paradigm. Machine and operating system paradigms may also be wrapped in object-oriented interfaces, thereby centralising all environment dependencies in one or as few places as possible. Anonymous parameters in Class Definitions ----------------------------------------- In C++ parameters in function templates are not required to be named, but just the type alone may be specified. For example a function f in a class header can be declared as f (int, int, char). This gives no clue to a class user as to the purpose of the parameter, without referring to the body of the class implementation. Meaningful identifiers are essential in this situation, as this is the abstract definition of a routine, and a client of the class and routine will need to know that the first int represents a 'count of apples', etc. The use of anonymous parameters handicaps the purpose of abstract descriptions of classes and members: to facilitate the reusability of software. Program text is the medium by which the meaning of the system is captured for some future activity, short or long term. Communication of intent of a software element is essential if reusability is to be achieved. A compiler strips away this level of communication, producing a machine executable entity. The careful production of a semantic entity should not be penalised by languages or compilers that perform less than optimal translations. But neither should a language definition allow less than optimal expression to the human reader. The reason that anonymous parameters may be in C is that it is not necessary to specify a name in a function template. But then naming as has been pointed out is not at all necessary in programming. Naming only exists to help the human reader identify different entities within the program, and to reason about their function. To this purpose naming is essential. Anonymous parameters may save typing in a function template, but then programming is not a matter of convenience, especially, where such a convenience is not a matter of convenience for later readers. This is an example of where redundancy is beneficial, as it saves a later programmer from having to go and look up the information in another place. A real convenience in function templates would be that abstract function templates be automatically generated from the implementation text (see header files for more details). The issue of anonymous parameters may be argued to be a safety issue, as a programmer may guess at what the purpose of a parameter is from the type, but guess wrong. However, it is often counter argued that C programmers always know what they are doing, and don't make this sort of mistake. If we ignore then that this could be a safety issue, it can still be labeled a 'courtesy' issue. Courtesy issues become even more important in the context of reusable software, as an interface client must know what the intention of the interface is for it to be used effectively. A courtesy implies an inconvenience to the one who provides the courtesy, but provides a convenience to one or more others. Courtesy issues include choosing meaningful identifiers, consistent layout and typography, meaningful and non-redundant commentary, etc. Courtesy issues are not just a style consideration, but should be reflected in, and supported by language design. A language may support, but cannot enforce courtesy issues, and it is often pointed out that poor, discourteous programs can be written in any language. But this is no reason for not being careful about the languages that we develop and choose for software development. In summary: the purpose of programming is to capture the semantics of a system. As a matter of style, meaningful identifiers should always be used in function templates in C++. Reusable software requires that the semantics of the abstract system are clear to enable that system to become a subcomponent of some other system. Courtesy issues are important if reusable software is to be realised, and should be considered in the design of a language. Constructors ------------ Multiple constructors are allowed by having different arguments, as for functions. This has the disadvantage that it precludes having two different constructors with the same arguments. Such constructors may be useful for initialising the object in different valid states. Constructors are also not named (apart from the same name as the class). This makes it difficult to discern from the class header the purpose behind the different constructors. Constructors suffer from all of the problems described with regards to functions with the same name but different signatures. It would be easy to mark routines as constructors, for example: constructor make (...)... constructor clone (...)... constructor initialise (...)... where each constructor leaves the object in valid, but potentially different states. Declaring named entities is a redundant activity, but it was realised in the transition from FORTRAN to ALGOL that such redundancy provides the mechanism which enables compilers to detect certain classes of errors. Naming is essential in the management of state space of a program, so the C++ implementation of constructors is at best an example of dubious merit. Constructors and Temporaries ---------------------------- When can 'return <expression>' result in some value other than the result of the expression being returned to the caller. In section 6.6.3, the C++ ARM says "If required the expression is converted, as in an initialisation, to the return type of the function in which it appears. This may involve the construction and copy of a temporary object (S12.2)." Section 12.2 explains "In some circumstances it may be necessary or convenient for the compiler to generate a temporary object. Such introduction of temporaries is implementation dependent. When a compiler introduces a temporary object of a class that has a constructor it must ensure that a constructor is called for the temporary object." A note says "The implementation's use of temporaries can be observed, therefore, through the side effects produced by constructors and destructors." Now putting this together, creation of a temporary is implementation dependent, so may or may not be done. A constructor is called as a side effect, and this can completely change the value held by the object if it wishes. But as different implementations may decide to do different things with temporaries, this means that the potential exists that different results may result from different C++ implementations. Optional Parameters ------------------- Optional parameters which assume a default value according to the routines declaration are supposed to provide a shorthand notation. Shorthand notations are intended to speed up software development. Such shorthand notations may be convenient in shell scripts, and interactive systems, but in large scale software production, where precision is mandatory, can lead to ambiguities and mistakes. Firstly, optional parameters open up the possibility that the programmer may assume the wrong default for a parameter. Secondly, it is possible that the programmer did not intend to omit the parameters (most errors being, what the programmer did not intend), but optional parameters mean that the compiler must assume that this is correct, and this bypasses the safety check that parameters must match exactly those defined in the function header. Furthermore, they do not provide a great deal of convenience. If a routine has five parameters, the last three of which are optional, and caller wants to assume the defaults for parameters 3 and 4, but must specify parameter 5, then all five parameters must be specified. This mechanism can be easily provided by other means, already in the language. For example, you can make a parameter optional by having a call to another (inline) routine which provides the default. This not only provides the convenience of optional parameters, but is more powerful, as any parameter or combination could be filled in with any combination of defaults, not just the last parameters. Multiple sets of defaults may be provided by multiple intermediate routines. In summary: optional parameters provide little convenience, but open up the possibility for errors. Every convenience, and more, of optional parameters may be provided by other language mechanisms. In short, use these alternative mechanisms in preference to optional parameters. Bad deletions ------------- The following example is given on p.63 in the C++ ARM as a warning about bad deletions which cannot be caught at compile-time, and probably not immediately at run-time: p = new int[10]; p++; delete p;// error p = 0; delete p;// ok One of the restrictions of the design of C++ is that it must remain compatible with C. This results in examples like the above, which are ill-defined language constructs, which can only be covered by warnings of potential disaster. To avoid such language deficiencies would result in loss of compatibility with C, but this may be a good thing if problems such as the above disappear. But then the resultant language may be as far removed from C as to suggest that C is abandoned altogether. Local entity declaration ------------------------ The ability to declare an entity close to where it is used, has both advantages and disadvantages. It is a convenience, as you can declare the entity close to where it is used, but this can make a routine appear more complex and cluttered. Another potential problem is that an identifier can be mistakenly overloaded within a nested block in a function, with the resultant problems covered in name overloading and nesting. This simple form of name overloading was used in ALGOL (name overloading has been around a long time, in fact the designers of Simula realised this, and hence object-orientation was born, although they thought it was just structured programming if you read the book by Djikstra, Hoare and Dahl). As has already been pointed out in the section on name overloading, overloading is best provided in limited by well controlled forms. C originally avoided this problem by not having nested routines or blocks, (a block in the ALGOL sense contains both declarations and instructions.) It should also be of concern that the local entity declaration scheme of C++ does not ensure the 'separation of concerns', as described in the section of information, computation, and execution mechanisms, giving the benefits of data independence, and implementation transparency. This is important in large scale software development, and protection of programming investment, should the requirements of the system be changed. The ARM should be consulted for further problems of local declarations vs branching, which shows the complications in intermingling declarations and instructions. Again fixing poor language definition by caveats cannot make up for the faulty definition. The C++ FAQ (Q83) is not very clear on this point (although it is mostly excellent), claiming that an object is created and initialised at the moment it is declared. This only applies to auto, in stack objects. Dynamic entities are not created and initialised until they are subject of a 'new' instruction. In well written object-oriented software, routines will be small, typically performing one atomic action per routine*, and therefore unneeded entities will not be constructed until the routine is actually called. This strategy means the provision of such locals is less likely to be of any benefit. If as the FAQ suggests, that initialising an object is so expensive, then the overhead of a routine call will be negligible, thereby realising the efficiency gain by good modularisation by subroutines. Further if an object is in the stack, this suggests that its constructor should be simple, only initialising the object to some neutral state (like integers to 0, booleans to false, etc), leaving the bulk of the work to where the object is actually used. Thus a complex constructor can be broken up into a simple part, which just gets the object going, and a more complex part in a normal routine, which is called closer to the time of use. On the whole, this seems to be a better modularised approach. In fact allocating all locals at the beginning of a routine may be an optimisation on some processors (like the M68000), where the stack space for all locals is allocated in one instruction by adjusting the stack top, rather than many little increments for each individual declaration statement. Other than that, the FAQ offers some excellent general advice on optimisation in this section, as you should not embark on expensive creation operations before doing some cheap checks to see whether that is necessary. Also the criticism in the FAQ of 'set' methods is quite useful. *Small routines which implement atomic operations are fundamental to loose coupling. For example, a base class that provides a routine that logically performs operations A and B, is not useful to a subclass which wants to perform A as it was in the base class, but needs to provide its own implementation of the logic of B. This way the descendant is forced to reimplement the logic of both A and B, missing an opportunity to reuse the logic labeled A. Such tight coupling is to be avoided, as it reduces flexibility. In summary: due to the typically small routines written in object-oriented systems, declarations intermingled with instructions result in very little convenience, and if the system is designed correctly, will probably not be used a great deal. Local declarations lead to complications, and also can result in unintended name overloading, due to nesting. Use of local declarations means that information and computation concerns are not cleanly separated. Friends ------- Friends provide a mechanism for overriding data hiding. A friend class or function of a class has access to the private data of a class. Friend is a 'limited export' mechanism. The friend function or class has access to all the members in the private section of a class. There are three problems with friends: 1) They can change the internal state of objects from outside the definition of the class. 2) They introduce extra coupling between software components, and should therefore only be used sparingly. 3) They have access to everything, rather than being restricted to only the things that are of interest to them. The usefulness of friends shows that there is a case for shades of grey between public, protected and private members. The functionality of friends may be provided in ways that do not have the above three problems, by ways which are consistent with the object-oriented paradigm, while not requiring the introduction of a foreign concept such as friend. The programmer may make careful use of such mechanisms by careful class design, which leads to as little coupling between components as possible, in conjunction with a selective export mechanism. A selective export mechanism (more general than public, private, protected and friend), will also document the couplings between entities that can occur in the system, as does friend. Selective export specifies that not only should a member be exported but to which other classes specifically to which it is to be exported. One reason given for the use of friends, is that it allows efficient access to data members, which would otherwise have to be accessed via a function call. The way C++ is often used is that data members should not be put in the public section, as this breaks the data hiding principle. Data hiding may be better described as 'implementation hiding'. The interface to the external world, exports entities that return values of given types, so there should be no restriction on the export of variable members. The strategy is that entities should not be exported if they expose any detail of implementation, and that exported variables are treated by the outside world in the same manner as functions, ie they cannot be assigned to. This is because, when used in expressions, there is no semantic difference between functions and variables, as they both return values of a given type. (See fn () for an explanation of why variables and functions are best regarded as similar entities.) (See also Marshall Cline's explanation of friends in the FAQ for further clarification of the friend concept.) The Cambridge Encyclopedia of Language has an interesting point about public and private names. It says "Many primitive people do not like to hear their name used, especially in unfavourable circumstances, for they believe that the whole of their being resides in it, and they may thereby fall under the influence of others. The danger is even greater in tribes (in Australia and New Zealand, for example), where people are given two names - a 'public' name, for general use, and a 'secret' name, which is only known by God, or to the closest members of their group. To get to know a secret name is to have total power over its owner. In summary" The concept of friend may be provided more generally and consistently by selective export, which specified which entities are to be exported, and to which classes. Static ------ Static is a word that causes much confusion in C++. This confusion is mentioned on p. 98 of the C++ Annotated Reference Manual (ARM), which claims it has two meanings. Actually it has three meanings. Firstly, as above, a class may have static members, and a function may have static entities. The second meaning comes from C, where an entity declared static is local in scope to the current file. The choice of independent keywords would easily solve this trivial problem. The third meaning is a general object-oriented meaning, not specific to C++. It is that objects may be statically allocated, as opposed to dynamically allocated in free space, or automatically allocated and deallocated on the stack, when a block is entered. Static class members are a useful concept, but of limited use, and the functionality can easily be provided without the extra notion of static. On page 181 of the ARM, the reason is given that statics will reduce the need for global variables. Such members though may be placed in a class of their own, and one copy only of this class is instantiated (a global object). Each object is instantiated with a reference to the object with the static data. For example class static_data { // Only one object of this class is instantiated. ... static member declarations } class original_class { static_data * our_statics; } This is a clue as to why global entities are unnecessary in object-oriented programming. (See the note on name overloading and nesting, and global environments.) Static may also be applied to declarations in functions. These are not needed in an object-oriented language. The reason and history is this. ALGOL had the notion of 'OWN' locals in blocks. The semantics of an OWN entity was that on every entry to the block, it preserved the value from the last entry to the block. The implementation of this was that at run time, it was kept in the global environment, but at compile time, the scope of the OWN entity was limited to within the block in which it was declared. Now this caused complication in recursion, as the same instance of the variable was used in all invocations of the procedure, rather than each invocation using separate local storage on the stack. The designers of Simula generalised the ALGOL notion of block, so that instead of deallocating the storage on block exit, that storage could be made 'persistent', and so object-oriented classes were born. Now since the declarations in this 'class' block were no longer deallocated, this removed the need for the OWN concept, which had proven problematic in ALGOL, so there is no OWN in Simula. So there is no reason for static or OWN declarations in object-oriented languages, as this is achieved by making the static or OWN a class member, if persistency is required in the lifetime of the object, or if this value is required to be shared among objects, then it may be provided by a shared object, as suggested for static class members. If persistency is required for the lifetime of the program, then this shared object will not be deleted until the program is terminated. In summary: the static keyword and concept has been confused by overloading one word with different semantics. The object-oriented concept of class is very simple, yet very powerful. It is the basis for a consistent division of a programs state space. The power of this method is that other mechanisms for storing permanent data in a running program are not needed, and only complicate systems. The mechanisms that are not needed are globals, static members, and static declarations (OWNs). Union ----- As with static, this is another construct not needed in OOP. Firstly, similar concepts have been recognised as problematic in other languages, for example, FORTRAN's equivalences, COBOLs REDEFINES, and Pascal's variant records. Such mechanisms when used for overloading the same memory space forced the programmer to think about how achieve this. The stack mechanism in recursive languages meant that this was not necessary, as memory was automatically overloaded for the locals of procedures, and the same space was reused as procedures were entered and exited. So this use of union when used similarly to FORTRAN's equivalences is not needed. Union is also not needed to provide the equivalent to COBOL REDEFINES or Pascal's variants. In OOP though, this is provided for by inheritance. A reference declared as a superclass may be used to refer to all subclasses, and this way provides the same semantics as union, only in a type safe manner. A declaration of a reference to a class is implicitly a union of all subtypes of the class. Nested Classes -------------- Nested classes were provided in Simula, in much the same way that ALGOL provided nested procedures. This was later realised to be of questionable value, as it was not in the free spirit of object-oriented decomposition, where classes should be loosely coupled, thereby supporting software reusability. C indeed did not have the complexity of nested functions, but C++ has chosen to implement this complexity for classes, which is of less use than nested functions. Nesting is achieved more generally by inheritance. In C++, not only may classes be nested within other classes, but they may also be defined within functions, thereby tightly coupling a class to a function. The concept of object-oriented programming is that the class is the fundamental structure, and that nothing has existence separate from class (including globals), but C++ is confused as to whether it is procedure-oriented or object-oriented. While the ability to able to have classes local to functions may not cause any practical problems aside from precluding the class definition from being reusable, it causes theoretical and conceptual problems, which should not be ignored as nested classes are of little practical use. Of course examples may be given, but such examples may also be structured in simpler, more straightforward ways, without loss of generality. (See also the general comments on name overloading and nesting.) Classes that can be nested within other classes and functions also raise questions about the usefulness of this approach to multi-paradigmed programming. It seems that the object-oriented approach will be most useful if it is used for the overall organisation of software, encapsulating other paradigms if necessary. However, C++ has combined paradigms in such a way, that object- oriented encapsulation, and the advantages that it realises are no longer guaranteed. Of course it may be pointed out that there are examples that may be written using the C++ approach, but maybe such examples may be better left to academia and research, rather than trade off consistency, which will be most beneficial for software production houses, which require commercial grade tools, rather than a general purpose research harness. Global Environments ------------------- C++ functions may change the global environment, beyond the object in which they are encapsulated. Such changes are side-effects that limit the opportunity to produce loosely-coupled objects, which is essential to the production of reusable software, as it is difficult to remove the class from its surrounding environment. This is a drawback of both global and nested environments. In fact the issue of side-effects is the most consistently addressed issue in the OO world. A good OO language will permit changes to an objects state to be made only by routines in that object. Removal of a global environment is a technically trivial exercise. It is simply the requirement that the global environment be encapsulated in an object or set of objects of its own, after all that is what objects do: provide an encapsulated environment, and there is no reason why the global or external environment should not be encapsulated. Such objects facilitate a clean object-oriented interface to the environment external to the system, without loss of generality, for a negligible performance penalty. Thus classes are independent of a surrounding environment, and the project for which they were first developed, and are therefore easily adaptable to new environments and projects. Software written in C++ has the Unix style environment interweaved throughout programs. This is not the way to open systems and cross platform development. (See also the note on name overloading and nesting.) In summary: The object-oriented concept of class is very powerful, and changes the way software should be structured quite radically. Globals are problematic in that they provide a mechanism whereby everything has free access to them, rather than the disciplined access of class. Since globals are just an environment, it is simple to encapsulate this environment into an object or objects of its own. Header Files ------------ In C++ a class interface must be maintained separately from its body. While an abstract interface should be distinct from a concrete implementation, the interface and implementation may be both derived from one source. In C++ though, programmers must maintain the two sets of information. The drawbacks of replicated information are well understood. In the event of change, both copies must be updated, which could lead to inconsistencies, which must be detected and manually corrected. It is possible to provide tools that will automatically extract the abstract class header definitions from class implementations, and guarantee consistency. Also, the programmer must consciously make those headers of imported classes available. If the correct #includes are not specified, this results in a round of syntax errors. Another problem is that if header A includes header B, and header B includes header A then a circular dependency is set up. The same problem also occurs if header A includes headers B and C, and header B also includes header C. This is overcome by the simple but messy fix in all headers of #ifndef thismod #define thismod ... rest of header #endif Headers show how C++ addresses the problem of independent modules by a non-object-oriented approach which is sub-optimal as it requires the programmer to supply this information manually. A class interface is the OOP equivalent of a module header. A class definition contains all knowledge of component classes and their dependencies (inheritance and client) in the class text. Dependency analysis is derivable from the program text, and functionality of tools like 'make' can be integrated into the compiler itself, and the errors encountered in the use of 'make' are completely avoided. The mechanism for getting the header file information into a module is #include. #include is an old and not very sophisticated mechanism for the implementation of modularity. C++ still uses this 30 year old technique for modularisation, while other languages have adopted more sophisticated approaches, for example, Pascal with Units, Modula with modules, Ada with packages, and in Eiffel the unit of modularisation is the class itself, and all includes are handled entirely automatically. An OOP class is the more modern and sophisticated way of modularising programs, so there should be no need to support the #include mechanism in C++, at least for class dependency analysis, although required in order to remain compatible with C. Multiple inheritance is a form of #include. To clarify the relationship between modules and classes further, a module contains data and routines that manipulate that data. A system is assembled by combining modules. A class also contains data, and routines that act on that data. An object-oriented system is assembled by the combination of classes. Modules are a primitive form of classes. Classes are more sophisticated, as they express with much more precision the relationships with other classes. C++ #includes and modules have problems, and this primitive building block method is not required in an object-oriented language. In summary: In an object-oriented program, classes form the basis of modularity. The #include mechanism is old, and must be explicitly stated by the programmer, and is not necessary given all dependency information is implicitly derivable from the text of an object-oriented program. Multiple inheritance is a form of #include. The class interface is the object-oriented equivalent of the module header, so headers and #include mechanisms seem out of place in an object-oriented language. Class header declarations ------------------------- C's syntax for function declarations is [<type>] <identifier> (<parameters>). For (a very simple) example: class C { a (); b (); int c (); d (); char e (); virtual void f (); } To find an identifier in this layout, the eye must trace a course around the type specifications. This leads to a tiring activity, and a greater chance of the eye missing the sought identifier, so that the programmer must resort to using the search function of a text editor to help out. Other languages have chosen a different approach, placing the entities name first. For example: class C { a (); b (); c () int; d (); e () char; f () virtual void; } This seems backwards at first to those used to the ALGOL and FORTRAN styles of type first. But there is a very good simple reason for it. An example from the real world may illustrate the argument for name first. Imagine if a dictionary was published, and the keywords were not placed first, but rather the entry order was - noun /obvrzen/ obversion, the act or result of obverting the dictionary would not sell many copies, unless the marketers managed to convince large numbers of the population that there was something intrinsically magical about this order of layout, that made the explanation of the meaning more correct. This example illustrates how subtle syntax decisions can be very important, and why the PASCAL style of languages may have ordered things this way around, contrary to FORTRAN, ALGOL and others. These are trivial but important alternatives that the language designer must consider. In summary: layout of programming entities is essential for effective communication. Layout is provided by the dual roles of language syntax, and programming style. A dictionary or index style layout would suggest placing entity names first, followed by their definition. Calls on references to deallocated objects (Dangling Pointers) -------------------------------------------------------------- The compromise that C++ does not have garbage-collection means that it is possible that a routine in a deallocated object may be called. For example if an object is deallocated in one place in the program, but another part of the program still has a reference to this object, then the potential to invalidly invoke a routine exists. There is no way to detect that the object has been deallocated, as the reference will not be NULL, but a 'dangling reference'. This contributes to the fragility of C++ programs. The second problem is that objects which become useless may miss being deallocated. This leads to memory leaks, which means memory gradually becomes full of dead objects, leading to eventual system failure. Such memory leaks are difficult to detect and find. Such leaks are often only detected after long and thorough testing of the system. Both problems are solved by garbage-collection. Garbage-collection has undeservedly earned a bad reputation due to some early implementations exhibiting some performance problems, instead of working transparently in the background, as they can and should. As C++ does not include garbage-collection, the problems are often over-emphasised as a justification for C++ not including it. It may be argued that the lack of garbage-collection in C++ is not an engineering compromise, as its inclusion would be an engineering impossibility, as there are many ways that a programmer can undermine the structures that are required to implement correctly working garbage-collection. While garbage-collection may not actually be an engineering impossibility in C++ (EC++), it is difficult, and programmers would have to settle for a more restricted way of doing things. This may be a good thing, but then the compromise to remain compatible with the C language becomes difficult, if compiler detection of practices inconsistent with the correct operation of garbage-collection is required. Type-safe linkage ----------------- As explained in the C++ ARM, type-safe linkage is not 100% type safe. If it is not 100% type-safe, then it is not safe. It is always the subtle errors that provide the most problems, not the simple or obvious ones. Such subtle errors get into the production system, undetected, until a maybe critical moment. The seriousness of this situation cannot be underestimated. As many forms of transport, such as planes, and space programs depend on software to provide safety in their operation, lives can depend on software. Not only this, but often the financial survival of organisations can depend upon their software. Therefore the acceptance of such unsafe situations is at best irresponsible. The C++ ARM summarises the situation as follows - "Handling all inconsistencies - thus making a C++ implementation 100% type-safe - would require either linker support or a mechanism (an environment) allowing the compiler access to information from separate compilations." So why does the C++ compiler (at least AT&T's) not provide for accessing information from separate compilations, and why is there not a specialised linker for C++, that actually provides 100% type safety? There is no reason why C++ should not be implemented in this manner. Building systems out of preexisting elements is the common Unix style of software production. This implements reusability of a form, but not in the truly flexible manner of object-oriented reusability. This is why in the future, Unix may be replaced by completely object-oriented operating systems, which are indeed 'open', to be able to be tailored to best suit the purpose at hand. By the use of pipes and flags, Unix software elements can be reused to provide some sort of functionality that approximates what is desired.This approach is valid and works with efficacy in some instances, like small in-house applications, or perhaps for research prototyping, but is not acceptable for widespread and expensive software, or safety critical applications. Since C++ is becoming widely used, this sort of implementation should not be regarded as acceptable. For C++ implementations that do not have their own specialised linker, optimisations such as the replacement of virtual calls with procedure calls may not be possible. C++ and the software lifecycle ------------------------------ Much work has been done on defining the software lifecycle. It is at least generally accepted that the activities in the lifecycle are analysis of requirements, design, implementation, testing and error correction, extension. Unfortunately, the result of identifying these activities has resulted in a school of thought that the boundaries between these activities are fixed, and that they should be systematically separate, one being performed and completed before the next is commenced. It is often argued that if they are not cleanly separated, then you are not practicing disciplined system development. This view is quite incorrect. Someone who sits down and starts writing program text straight away is actually doing all the steps in parallel. It may not be the best way do do things in many circumstances, may or may not suit the style and thinking of different people, but there are scenarios where this works, and can be the methodology of choice of disciplined thinkers. More likely though, the phases will interact and overlap. Facts found out only as late as the implementation stage may be fed back into the analysis and design stages. It is now becoming accepted that the software lifecycle should be an integrated process, that each phase will lead to the next naturally, without unnecessary artificial separation, and that the activities can progress in parallel, expediting software development. The object-oriented approach supports this model well. When the steps are artificially separated, this leads to a large semantic gap between the steps. The transformations required to bridge such semantic gaps are prone to misinterpretation, time consuming and costly. What is required in the software industry is a more rational approach. We should have learnt from the extremes SA/SD. This methodology was taken to mean in some quarters that the methodology was all important, while programming and programming languages were not at all important. This attitude was strengthened by programming languages being arcane and machine-oriented. A modern software language will support the integration of the activities of design and implementation by being readable, and problem-oriented. A modern language needs to be as close to the design as possible, as the needs and requirements of an enterprise can change much more rapidly that the programmers can keep up, especially in a highly competitive and commercial world. So how does C++ fit into this picture? Well it is based on C which was designed mainly as an implementation and machine-oriented language. It is an old language, which did not need to consider the integrated lifecycle approach. C++ may have some of the trappings of object-oriented concepts, but it is the marriage of a problem-oriented technique with a machine-oriented language. It addresses implementation, but not so well the other aspects of the software lifecycle. Since C++ is not so well integrated with analysis and design, the transformation required to go from analysis and design to implementation is costly, as the semantic gap between design languages and the implementation language is great. We should have learnt from the structured world that this is not the correct approach to the software lifecycle. However, in the OO world we are again falling into the trap of artificially dividing up the lifecycle into distinct activities of OOA, OOD and OOP, instead of adopting an integrated approach. In summary: C++ provides limited support for design. Modern languages will provide a much more integrated approach to the complete software development process. C++ includes some of the structures from object-oriented programming, but does not address the entire software lifecycle, as a modern object-oriented language can. Reusability and Communication ----------------------------- Reusability is a matter of communication. In order to use a software component, you must be able to understand it. To do so, the writer must communicate the purpose and intent of the software to the client, and the correct usage of that software. In the object-oriented world, clear and concise definition of software modules is therefore not a mere nicety, but essential if reusability is to be realised. Arising out of the issue of reusability is extendibility. In order to maximise the reuse of software, it will often need to be tailored to suit the new application at hand. The client programmer in this situation must decide if the software component is indeed suitable for the task in hand, and if so, what is the best way to extend it. Reusability is a matter of Trust -------------------------------- Reusability is a matter of trust. If you do not have confidence in a software component, then it will be difficult to reuse. Matters for doubt may be, that it does not provide enough functionality, or even correct functionality, or that it may not be efficient enough for the purpose, or worse still may crash. If the software component cannot be trusted, then it is not a potential candidate for reuse. The C/C++ philosophy that checks are not built into the language and compiler, because programmers can be trusted, works against reusability. If you are a potential client of a software component, then you need to be reassured that the component is as trustworthy as possible. Programmers make mistakes, and if you have the uncertainty that certain classes of mistakes have not been caught by the compilation system, then confidence in reusability is undermined. The C/C++ philosophy of trusting programmers is contrary to the object-oriented goal of reusability. In the real world of reusability, the furry headed ideal of trusting programmers is not appropriate. In reality, customers doubt the claims of suppliers. It is the onus of the supplier to prove their claims, and thus trustworthiness of the software. The client is not required to trust the suppliers programmers. The C philosophy of trusting programmers is against the commercial interest of both parties. Concurrent Programming ---------------------- In the next ten years we will probably be introducing into common use multiple processor arrays, which will execute concurrent programs. This requires much cleaner languages, than the single processor languages of today. Object-oriented concepts support concurrent programming, as objects may execute state changing code independently of each other. Concurrent programming will be enabled by the division of the state space of a system into modules to achieve a high degree of independent processing. Object-oriented programming provides a scheme to cleanly divide state spaces. In traditional sequential programming practice, the demand that everything be broken down into loosely coupled modules, that only interact through well defined interfaces may seem inefficient. However, it is precisely this scheme that will mean that concurrent solutions may be developed efficiently and transparently to the programmer. In concurrent programming, a thread in a unit of sequential execution. Concurrency is achieved by the splitting of threads. Threads may be split when a state changing routine is invoked, but not a value returning function, as the value must be waited on. State changing routines may easily be invoked on another processor. Object level granularity seems to be a natural candidate for concurrent processing. Only one thread may be active in a single object at any time to avoid simultaneous updates. Other levels of concurrency are instruction level, and task or process level. Task or process level is the level used in conventional multi-processing systems currently commercially produced, and instruction level is quite difficult, best being left to instruction pipelines. Object level is natural for the programmer, and has the advantage that a programmer can implement a system without taking into account parallel processing at all. The same program will run irrespective of whether the customer is running a single processor, or a processor array. Concurrency is problematic where there is a global environment. C++ does not preclude the use of a global environment, and access to shared global data potentially causes a thread to lock, and where there are many such accesses, the advantage of concurrency is lost. Even the introduction of static entities does not help a great deal, as these still have the problems of sharing associated with concurrent programming. It may well prove to be the case that adapting C++ to a concurrent environment proves to be more technically difficult than the inclusion of garbage collection. In summary: Concurrent programming will require languages which purely express the requirements of the systems being implemented. Concurrent programming will require the simplest and most consistent programming paradigms, and extremely disciplined modularisation, so that a large state space can be updated concurrently. The division of state space into objects as in OOP is essential if concurrency is to be manageable, and used to the greatest benefit. Programmers will be able to produce more powerful systems by the acceptance and adoption of more 'restricted' practices. It is up to the compilers and target architecture to determine the optimal way to produce an executable system. The role of Language -------------------- For a light intermission between sections, there are some interesting points made by the Cambridge Encyclopedia of Language. It says that language is an emotional subject. "It is not easy to be systematic and objective about language study. Popular linguistic debate regularly deteriorates into invective and polemic. Language belongs to everyone; so most people feel they have a right to hold an opinion about it. And when opinions differ, emotions can run high. Arguments can flare over minor points of usage as over major policies of linguistic planning and education. Now while natural language may be difficult to be "systematic and objective about", does this apply to computer languages? While the definition of natural language is beyond our control, programming language definition is certainly within our control. Programming languages must both have the expressiveness of natural language, yet be precise and semantically consistent. As programming languages have rigorous requirements, we should be even more critical and objective about them. Unfortunately, it is a measure of immaturity in the programming profession that criticism is often met by emotional and irrational argument. This leads many to conclude that the choice of a programming language is no more than a religious issue. This is incorrect as the choice of a good programming language should be done on technical merit, and there are technical measures by which a language may be judged effective or not. If language choice is merely a religious issue, then we may as well still be programming in assembler, or maybe even binary. Understanding the role of language will help quantify what it is we have to measure. The encyclopedia lists several functions of language. "To communicate our ideas", it says is the most common answer, and this must surely be the most widely recognised function of language. Other functions it lists are: emotional expression, for instance, when we stub our toe, we often emit words, even when there is no one to hear; social interaction, for example if someone sneezes, we often "bless" them; the power of sound, as in poetry and rhyming jingles etc; the control of reality, as in spells and incantations, perhaps computer programs and spells are very close in purpose; recording the facts, record keeping, historical and geographical documents, etc; the instrument of thought, we quite often reason about things to ourselves in language; the expression of identity, it can tell the world who we are, or affirm our belonging to certain groups. Perhaps the most important role of computer languages is that of description. "Language shapes the way we think, and determines what we can think about." - B.L.Whorf. As mentioned before, this is quoted by Bjarne Stroustrup. But how true is this? The encyclopedia says, "It seems evident that there is the closest relationship between language and thought: everyday experience suggests that much of our thinking is facilitated by language. But is there identity between the two? Is it possible to think without language? Or does language dictate the ways in which we are able to think? Such matters have exercised generations of philosophers, psychologists, and linguists, who have uncovered layers of complexity in these straightforward questions. A simple answer is certainly not possible; but at least we can be clear about the main factors which give rise to complications. Indeed it seems that there can be both verbal and non-verbal thought. For example following a road map in a car. But when we try to put this into words, we find it quite a tedious task. The above Whorf quote is a statement of the position of the Sapir- Whorf hypothesis on language and thought. This was formulated by Edward Sapir (1884-1939) and his pupil Benjamin Lee Whorf (1897- 1941). It reflects the view of the day that great value was placed on the diversity of the languages and cultures of the world. The Sapir-Whorf hypothesis combined two principles. The first is 'linguistic determinism', which states that language determines the way we think. The second, 'linguistic relativity', that the distinctions found in one language are not found in any other. The Sapir-Whorf hypothesis in its strongest form, as in the Whorf quote, is not generally accepted now. For one reason, it is known that concepts can be translated from one language into another, even if in one language, the concept may be expressed in one word, but takes a longer explanation in another. A weaker version of the Sapir-Whorf hypothesis is accepted. That is that "language may not determine the way we think, but it does influence the way we perceive and remember, and it affects the ease with which we perform mental tasks. Several experiments have shown that people recall things more easily if the things correspond to readily available words and phrases. And people certainly find it easier to make a conceptual distinction if it neatly corresponds to words available in their language. Some salvation for the Sapir- Whorf hypothesis can therefore be found in these studies, which are being carried out within the developing field of psycholinguistics." The important question to the programming community is do programming languages 'shape' the way we think about and design systems. The negative argument suggests that it is the concept behind the languages that are important, not the languages themselves, as they only provide a framework for the expression of the concepts. A language can only be as good as the concepts it implements, but a language must also be a simple and clean implementation of the concepts, perhaps expressing the concepts in the equivalent of as few words and constructs as possible. Programmers who understand the concepts should have no difficulty in adapting to different languages, as long as the concepts are implemented elegantly in the new language. A language may be judged like a wine connoisseur judges wine, by holding it up to the light to judge it for clarity and colour. Ultimately, it is the taste that matters, but good colour and clarity suggests that the taste is more likely to be good. So with programming languages, the clarity of the definition of the language will help that which is important, producing quality software using that language. So where does this leave Sapir-Whorf with respect to programming languages? Programming languages do not shape the way we think. It is the concepts that shape the languages, and it is the way we think that shapes the concepts. Those who have attempted to learn a language in order to learn object-oriented programming realise that it is the concepts which must be grasped in order to be effective. Once the concepts are learnt, object-oriented programming seems a most natural way to program, as it matches most effectively the way we think. If C++ has been designed according to the Sapir-Whorf hypothesis, its philosophical basis is quite at odds with a computer industry that should shape tools best suited to its purposes, processes, thinking, and concepts. Quotes taken from "The Cambridge Encyclopedia of Language", David Crystal, (Cambridge University Press 1987). Generic C criticisms ==================== These criticisms apply to the C base language, but in general adversely affect C++. Pointers -------- The C pointer mechanism requires the programmer to be concerned with issues that the compiler can automatically take care of. Firstly, the programmer must be concerned with the underlying address mechanism. Secondly, the programmer must be concerned with the correct dereferencing of pointers to access the referenced entity. Many languages make both concerns transparent to the programmer, being naturally the responsibility of the compiler to generate such code, with no loss of generality, or efficiency. An example is that the C programmer has to worry about the means of parameter passing, and must work out the correct use of &s and *s. If this is incorrect, at best a syntax error may be produced, at worst incorrect code will pass into testing and production. This information is specified in the parameter declaration, so that the programmer needs no longer worry about the access mechanism in the body of the routine. This again enables flexibility, as the use of the parameter is implementation independent. The pointer mechanism of C means the programmer must constantly be aware of the implementation mechanism. This is against the principle of OOP that all data access should be achieved in an implementation independent manner. C++ rectifies this to some extent by the introduction of reference variables (is this an admission that the other languages were correct all the time?) This is another syntactic variation that programmers must be aware of. Another small problem is the confusion caused by the style of declaration - int*i, j; This does not mean, as might be easily read - int*i, *j; but int*i, j; and should be written thus to avoid confusion. Another consideration is that the mechanism for implementing dynamic memory space may change from one target environment to another. For example on the Macintosh, relocation of objects is implemented by a double indirection mechanism (handles). This is similar to the Unisys A Series mainframe approach of having all object descriptors access the target object via a master descriptor, which stores the actual address of the object. However, the A Series makes this completely transparent to programmers in all languages. Different implementations may not allow relocation at all, and thus the need for double indirection imposes an unnecessary performance penalty. Such details should not be the concern of the programmer, but rather the compiler in the target environment. The manipulation of C pointers is error prone. A pointer may be incremented past the end of the entities they are pointing at, with subsequent updates via the pointer possibly corrupting other entities. How many lurking and undetected errors may be in programs because of this? This again illustrates how C undermines OOP by providing a mechanism where state outside of an object's boundaries may be changed. This problem is exacerbated by the fact that pointers are intrinsic to the method of writing software in C, and the techniques are so widely used that many programmers are unable to understand how software can be produced without their use. Pointers as implemented in C make the introduction of advanced concepts like garbage collection and concurrency difficult. Arrays ------ As noted on p 137 of the C++ ARM, C arrays are low level, and yet not very general, and unsafe. Modern software production has far less dependence on arrays than in the past, especially in the object-oriented environment. The trade off to be optimal, rather than general and safe no longer applies for most applications. C arrays provide no run-time bounds checking, not even in test versions of software. This seriously undermines the semantics of an array declaration, ie that an array is declared to be of a particular size, and can only be indexed by values within the given bounds. If also compromises software safety. Also, in C there is no notion of dynamically allocated arrays, whose bounds are determined at run time, as in ALGOL 60. This limits the flexibility of arrays. Thus the C definition of arrays compromises both program safety and flexibility. Arrays are in general just another object-oriented entity, and should be treated in an object-oriented manner as a class of data structure, to which are applied the interface definitions, and consistency checks inherent in object-oriented systems. D.C. Ince in ACM SIGPLAN Notices, January 1992, in an article "Arrays and Pointers Considered Harmful", explains why arrays and pointers need not be relied upon so heavily in modern software production, as higher level abstractions such as sets, sequences, etc are better suited to the problem domain. Arrays, and pointers may be provided in an object-oriented framework, and used as low level implementation techniques for the higher level data abstractions. As has already been mentioned object-oriented programming is very useful for the encapsulation of implementation and environment oriented details. Ince suggests that arrays and pointers should be regarded in the same way as gotos in the seventies. He suggests that languages such as Pascal and Modula-2 should be regarded in the same way as assembler languages in the seventies. Perhaps even more so should C and C++ be regarded, as pointers and arrays are far more intrinsic in the use of C and C++. Function Parameters ------------------- One use of pointers is to simulate by-reference parameters with by-value parameters. Being able to distinguish between by-value and by-reference parameters is not just a syntactic nicety, included in most high level languages, but a valuable compiler technique. It means that the compiler can detect where dereferencing is needed, and can generate code accordingly, without requiring the programmer to be concerned about this. However, most languages do not even get this correct, allowing assignment to the local copy of by-value parameters. By-value parameters should be read-only, and not allowed to be the target of an assignment within a routine. By reference parameters mean that the value of a parameter passed to a routine may change. This introduces a mechanism of updating the state space, other than straight assignment (although the routine may use assignment to achieve the 'dirty deed'.) Such value changing mechanisms introduce side-effects. Object-oriented principles suggest that parameters should not be by-reference at all, whether given direct language support, or simulated support as in C. OOP realises that by-reference parameters are another mechanism that allow side-effects, changing the environment beyond the boundary of the current object. Parameters need only be read-only, and if a change is required to an object passed as a parameter, then it must be performed by calling a routine encapsulated in that object. This removes the possibility of the side-effects of by-reference parameters, and means that an object cannot change the environment outside of itself by assignment to a by-reference parameter within a routine. That an entity may be updated via a point external to itself, is quite opposite to the data hiding principle of object-oriented programming. void * ------ "Passing paths that climb half way into the void" - Close to the Edge, Yes Is this the programming equivalent of an oxymoron? That you can declare a pointer to void suggests some sort of semantic nonsense, a dangling pointer perhaps? While we can have some fun conjecturing what some of the obscure syntax of C++ may suggest, a serious problem is that void * declarations are used to defeat the type system, and thereby compromise the advantage of having a type system. A well thought out type system requires no such facility. When a typed entity is assigned to a reference of void *, it looses all its static type information. When it is assigned back to a typed reference the programmer must explicitly specify to the compiler the type information. This should at least result in a run-time check, to make sure that the correct type actually is being assigned. Without type checks, the routines of one class may be mistakenly applied to objects of another class. void fn () ---------- The default type that a function returns is int. The default should be a typeless routine, returning nothing. This is an example of where C's syntax is not well matched to the concepts and semantics. Syntactically no <type> should suggest nothing to return. In C though the function must be declared void. Also a typed function may be invoked independently of an expression. It is a shorthand way of discarding the value returned, which means the compiler does not detect that the function invocation has failed to match the functions signature. Thus an important type of consistency check is not made. Values should be returned because they need to communicate with the outside world, and ignoring returned values is often dangerous. fn () ----- Empty parenthesis is the invocation operator in C. This relates C to languages such as FORTRAN and COBOL, where the specific CALL and PERFORM instructions were required to indicate routine invocation. This similarity of C to FORTRAN and COBOL is not so obvious, as the () operator is more mathematical looking in nature. In the ALGOL style of languages, invocation is automatically deduced by the compiler when it sees a routine name, as it knows that the identifier refers to a routine. This compiler technology was not realised at the time FORTRAN and COBOL were developed. This compiler technology is possible, because much information is stored by the compiler about the entity, and the compiler can check that subsequent use by the programmer is consistent with the declaration, and in many cases the compiler can be relied upon to generate correct code, without having to burden the programmer with having to redundantly supply this information. This contributes to flexibility and implementation independence. A specific invocation operator results in an artificial distinction between functions and variables. Indeed, a variable used in an expression is a value returning function, it just returns the value stored in the variables memory location, rather than calling a function to compute the value. Functions and variables are the implementation viewpoint of the problem-oriented concept of attributes. Attributes may be both static, and dynamic. For example, a printer may have static attributes like pages per minute, printer type (laser, line, etc). A printer may also have dynamic attributes, like the number of print jobs queued, pages left in the current job, etc. However, the user of the attributes should not be concerned, whether they are static or dynamic, just that the interface returns a value of a particular type. This view also ensures implementation independence. The important semantic information of an identifier to the programmer is that it returns a value of a given type. The programmer should not care whether the value is the result of a routine, or the value stored in a memory location. What is important is that the value is of a type compatible with the expression in which it occurs. This can be checked by the compiler. The function invocation distinction reduces flexibility. If for optimisation, a function is replaced by a precomputed variable, it is not possible to use the same name without removing all the () from the original function invocations. This may be spread over many files, and such optimisation may not be attempted by the programmer to avoid the tedium associated with the task. Thus implementation detail is visible for the outside world to see. If the rarer circumstance of needing a reference to a function is required, then this special situation should have the extra explicit syntax, not the common invocation situation. Having such function pointers though is analogous to the call by name facility in ALGOL, and this was recognised as having pitfalls. All these pitfalls may be avoided by consistent application of the object-oriented paradigm. A common use of function pointers is to explicitly set up jump tables. The mechanism behind virtual functions is a jump table of function pointers, and the design of a program may take advantage of this fact, without resorting to explicit jump tables. Another use is to jump to a function in a table, which is indexed by an input character. This mechanism is better catered for by a switch statement, which makes what is meant explicit, while keeping underlying mechanisms (and possibly optimisations) implicit. C++ allows function pointers to member functions to be stored in tables (via the .* and ->* operators). This though cannot take advantage of dynamic dispatch through the virtual jump table. In summary: Mathematically, any entity which returns a value of a given type is a function. This includes variables used in expressions, so distinguishing between variables and functions introduces an artificial distinction into the language. This reduces flexibility. Distinguishing between static and dynamic attributes, reduces implementation independence. The () operator is equivalent to the FORTRAN CALL, and COBOL PERFORM, only the syntax is different. How to handle an entity should be left up to the compiler, as the programmer has already specified the intended usage by the nature of the entities declaration. ++, -- ------ These are another unnecessary shorthand convenience. The shorthand += and -= is more powerful, as the variable may be incremented by values other than 1. This illustrates that there are no less than three ways to perform the same thing - a = a +1 a += 1 a++ ++a For full generality, only the first form is required, the others are a mere convenience. If it is mistakenly believed that the other forms produce more optimal code, then it should be pointed out that code generators, especially for expressions, can produce the best code for a target architecture. The last two forms a++ and ++a are the postfix and prefix forms. They are often used in the context of another expression. Thus one expression may be used to perform several updates. This is a very powerful and convenient feature, but introduces side effects into an expression, which sometimes have surprising effects, and may lead to program errors. The following example is given on p.46 of the C++ ARM - i = v[i++];// the value of 'i' is undefined This example the ARM points out should be detectable by compilers, but the exact interpretation appears to be left to the implementation, which may contribute to non-portability. It is better to program in a straightforward manner, as an optimising compiler will optimise well beyond what a programmer can do. An optimising compiler will analyse the surrounding code, and if an entity is used several times in a local scope, it will keep the value of that entity handy locally at the top of a stack, or in a register, rather than retrieve it from main slow memory several times. The nature of such optimisations depend on the machines architecture, which a programmer should not be aware of, or expected to be aware of, as open systems demands that programs should be able to be ported amongst diverse architectures and environments, very different to the original machine. As with name overloading, memory storage update is a problematic, but necessary part of programming. It should be provided by a language in a consistent and expected way. Most languages have realised that memory update is problematic, and typically only provide limited but sufficient ways of updating, by an assignment operation. (Many languages also have block memory copies as well, but block copy could also be provided by assignment.) Furthermore, most languages avoid side-effects by limiting updates to only one per statement. C provides memory update in too many ways. These ways add nothing to the generality of the language, but do add ways that program errors may be produced. In summary: Increment and decrement operators introduce no added benefits for optimisation with respect to modern compilation environments. These can introduce side-effects by multiple updates within one expression. These operators introduce no extra generality or power of expression into the language. Defines ------- The define declaration - #define d(<parameters>) has a different effect to - #define d (<parameters>) The second form defines d as (<parameters>). Extra white space between tokens should not affect semantic meaning of constructs. Case Distinction ---------------- While it is good to adopt some typographic convention for the presentation of names the fact that C distinguishes between upper and lower case in names can cause confusion. Whether a character is written 'i' or 'I' it remains the same character. We would not make the distinction if the i were italicised or emboldened. Case distinction is a weak form of name overloading. If you can overload names in such a way, you can easily use the wrong one. For example if an identifier is declared Fred, another one may be declared fRed. Such names may easily be mistyped or confused. Typographic conventions should not affect the semantics of a program (consider RPG). As every programmer will have experienced, one character errors are more difficult to detect than one would think. We are in general not good proof-readers. There is a psychological reason for this that the the brain tends to straighten out errors for our perception automatically. The human brain is an excellent instrument for working out what was intended, even in the presence of radical error. In order to overcome this programmers have to use their powers of concentration to override this natural tendency of the brain. Distinguishing upper from lower case in names only adds another level of difficulty. Modern language design takes into account such psychological considerations in these small but important details, being designed towards the way the human brain works, not towards the way computers work. Such considerations make a big difference to the effectiveness of people, but do not have any impact at all on the efficiency of code generated for the computer. What is more important, people or computers? As has been suggested, case distinction provides another form of name overloading. Name overloading is a double-edged sword, as it is the attachment of different semantics to the same name. This opens up the possibility of confusion, but it is difficult to imagine programming without it, and its corresponding notion of scope. In object-oriented programming, name overloading is used in a very powerful, but disciplined and restricted manner in order to limit the problems and confusion that are inherent to name overloading. Case distinction can result in unexpected name overloading that can cause confusion and errors. Name overloading as has been suggested in the section on name overloading should only be provided in controlled and expected ways. This means that where a name is overloaded in the same scope that the compiler should report an error, or require the programmer to explicitly state that this is the intention, as with Object Pascal's override. As another example, a commonly used technique is - class obj { intEntry; voidset_entry (int entry) { entry = Entry; } } Upper and lower case distinction also illustrates another old compromise that C made. The compromise was that upper and lower case letters are represented by distinct codes in the ASCII character set, and to remove this distinction required precious computer time. Most languages overcame this problem by an even worse restriction, that everything had to be in upper case. C's mix of upper and lower case seemed good in comparison, but it was still not correct. These old languages and C considered processor time to be more important to convenience for people. But 20 years ago perhaps it was, as computers were expensive and slow. This assumption has been removed a long time ago, but we are still stuck with it in C based languages, and the Unix operating system. Early compilers also had restrictions (original definitions of C say that only the first 8 characters of identifiers were significant), on the length of identifiers to between six and ten characters, and this meant that the upper lower case distinction gave the benefits of an extended character set. This consideration no longer applies in all but the most basic compilers. As an aside note, on a typically common practice in any language, due to historically short identifiers programmers shorten identifiers by the omission of vowels. This leads to error and ambiguity, and difficulty in reading and interpretation at a later date. For example do the characters 'prvt' represent 'private' or 'pervert'? This slows good typists down, and poor typists probably even more, while they work out exactly which vowels to miss out. This shows that many programmers have not adapted their practices as the limitations of technology have been removed. In summary: The language designer has two alternatives. To either have case distinction or not. Case distinction can lead to rare but difficult to detect errors, due to yet another form of name overloading. The prohibition of case distinction may occasionally force the programmer to have to choose an alternative identifier, but the fact that a potentially erroneous name clash has occurred will have been detected early. The event of error from case distinction may be very rare, but then again would you trust your space shuttle to even the remotest chance of something going wrong, where billions of dollars are at risk? Assignment Operator ------------------- Using the mathematical equality symbol for the assignment operator is an example of a poor choice of symbols. Programming assignment is not equal to mathematical equality (:= != =). Language designers of ALGOL style languages realised they were semantically quite different, so took the care to distinguish, only using '=' to mean equality in the mathematical sense. In C the lack of distinction leads to error. It is easy to use = (assignment) where == (equality) is intended, and the result looks quite reasonable, resulting in errors that are difficult to detect. In fact this leads to a more general criticism of C, in that it has a pseudo mathematical appearance. There are very few people who are proficient at the interpretation of mathematical theorems, most passing over such sections in text, unfortunately often making the assumption that if the mathematics is there, the surrounding text has been proved correct. Generally, the pseudo- mathematical nature of C shares this bad attribute of mathematical notation as it is difficult to read, while lacking the semantic consistency and precision of mathematical notation. One of the keys to reusability is readability. Type Casting ------------ Type casting is just a specific form of mathematical function, which maps values of one type onto values of another type. Type casting has been useful in computer systems, as often, it is required to map one type onto another, where the bit representation of the value remains the same. Type casting is therefore a trick to optimise certain operations. Type casting provides no useful concept that cannot be implemented by more general functions. Furthermore, type casting causes problems in strongly typed systems, as it compromises the type system. In many languages, type casting is necessary, as the type system has not been consistently defined, so programmers often feel they have the need of the type casting mechanism, and may find it difficult to imagine languages where type casting is not required at all. Mathematically, all functions can be regarded as having a type casting function. An example often used in programming is to cast between characters and integers. Type casts between integers and characters may easily be expressed mathematically using abstract data types (ADTs). TYPE CHARACTER FUNCTIONS ord: CHARACTER -> INTEGER// convert input character to integer char: INTEGER /-> CHARACTER// convert input integer to character PRECONDITION // check i is in range pre char (i: INTEGER) = 0 <= i and i <= ord (last character) The notation '->' means every character will map to an integer. The partial function notation '/->' means that not every integer will map to a character, and it must be specified exactly which subset of integers will map to characters by use of a precondition. Now this may be provided consistently in object-oriented syntax with member functions on a class: i : INTEGER ch : CHARACTER i := ch.ord// i becomes the integer value of the character. ch := i.char// ch becomes the character corresponding to the value i. but a routine char would probably not be defined on the integer type so this would more likely be: ch.char (i);// set ch to the character corresponding to the value i. Now such basic data types, as character and integer, are usually catered for well by the hardware of most machines, and it is entirely possible that a compiler will generate code that is optimal for any target hardware architecture. However, such basic data types may be consistently and elegantly treated in the object-oriented paradigm, by the implicit definition of their own classes. Another example of type conversion is from real to integer. Here though, the programmer may wish to specify the use of two type conversion functions to truncate or round. TYPE REAL FUNCTIONS truncate: REAL -> INTEGER round: REAL -> INTEGER r: REAL i: INTEGER i := r.truncate// i becomes the closest integer <= r i := r.round// i becomes the closest integer to r Again many hardware platforms provide specific instructions to achieve this, and an efficient object-oriented language compiler may indeed generate the code best suited to the target machine. In summary: Type casting compromises the advantages of type safety. Type casting in a pure object-oriented system is not needed, as it is only a specific implementation of the more general mathematical notion of functions. Semicolons ---------- A program is a list of elements, and the executable part of a program is a list of sequentially executed instructions. Languages such as FORTRAN separated instructions by requiring that they be placed on different lines or cards. Elements in a list need to be separated in some manner, and the semicolon is one syntax for the separation of the elements in the list. The semicolon is therefore part of the syntax of the list, not part of the syntax of the individual instructions. But C makes the semicolon part of the syntax of each instruction by making the semicolon, a terminator of individual instructions. This elevates an unimportant semicolon to have a much higher syntactic value than it should. Imagine if the comma was a terminator, then function invocations would have to look like: fn (a, b+c, d, e,); C's handling of the grammar of semi-colons leads to an irregularity in if/else's: if (condition) statement1;/* Semicolon required */ else statement2; if (condition) { statement1; }/* Semicolon must be omitted */ else statement2; This is an irregularity, as both of the above will be reduced by a parser to the grammatical form: if (condition) statement else statement Conclusions ----------- C++ is overly complex. Object-oriented languages should provide sophisticated concepts in the simplest possible framework. Where the framework is not simple, the concepts tend to be lost, and there are many issues that OOP addresses in order to facilitate the production of complex and sophisticated programs. Many of these issues are addressed in implicit and subtle ways, but are lost in C++. There are many potential ways of introducing subtle errors into C++ software, and furthermore, the combination of these will cause even further problems. Programming is the orchestration of change within a large state space. Object-oriented techniques provide a method of simple division and management of such state spaces. To manage such state spaces requires the simplest techniques, in order to guard against detectable inconsistencies which may lead to errors in executable systems. C and C++ do not implement the simple management of a large state space, and also allow to many potential errors to go undetected. The role of a language as a tool cannot seriously be regarded as some authoritarian that stops us doing what we want or need to do, as many languages with type safety a consistency checks are often viewed. Programming languages should embody the collective wisdom of common sense practices, which have been learnt over many years, by common and painful experience. C++ lacks the implementation of much of this wisdom. It is better to detect and avoid errors than to fix them. The fixing of errors happens many times during the development process. This slows the development process down, and is therefore costly. Good programmers in this context (often labeled 'gurus'), are those who recognise symptoms, and recommend fixes. Good programmers in the better sense (often labeled 'impractical idealistic dreamers') adopt better practices (programming languages being a subset of these), that avoid error in the first place. The view that correctness checks are training wheels for students needs to be dispelled. Many disciplines have techniques to ensure correctness. For example, the metronome in music is not just for students, but will help an advanced musician ensure that the tempo of a piece is correct, and since playing to a metronome is more difficult, will help sharpen the musicians performance of the piece. The musician does not just view the metronome as a teaching aid, but as a tool that helps produce a polished and professional performance. This paper has shown many cases where C++ uses old C mechanisms to provide things that can and should be expressed consistently within the object-oriented paradigm. For example type casting. It is therefore suggested that the move to pure object-oriented languages will indeed facilitate more consistent programming, which will avoid many errors which typically occur in software production. The amount of change required in C++ to address the issues raised in this paper is seen as largely insurmountable. A programming language is just a tool, in the same way that an axe is a tool. In chopping down a tree, if the axe is blunt, then procedures, processes and methodologies may be invented to make the blunt axe as effective as possible. However, that does not solve the real problem that the axe that does the real work is blunt. So it is with programming languages. If a system is to be realised, then it must be implemented, and a programming language is the tool with which the real work is done. If the language is blunt, then procedures, processes and methodologies may alleviate that situation, but they do not solve the cause of the problem. Once the axe is sharpened, then real progress may be made, and the procedures, processes and methodologies also become more effective. A good axeman will have good axe wielding technique, but given a choice of axes will choose the sharpest implement. A poor axeman may not be effective with even a sharp axe, but that does not mean that the axe maker will not bother to produce the sharpest axe for the good axeman. The argument that poor programmers will produce bad programs in any language so we shouldn't bother with better languages is fallacious. The critique began with certain questions, and as no work can be absolute (particularly a programming language), it will end with more questions, which it is hoped will create more debate, and more questioning into what we are really trying to achieve with program development. Does C++ provide effective communication between programmers separated by both space and time? Does C++ provide communication between the levels of analysis, design, implementation and maintenance? Are the compromises made by C and C++ still relevant to today's environments, and the environments of the not very near future? Could C++ be regarded as the PL/1 of the object-oriented world, as PL/1 was the marriage of FORTRAN and structured ALGOL concepts, and C++ is the marriage of C with object-oriented concepts? Are the compromises made for the restricted machines and environments of 20 years ago still appropriate for today? Are language based on 20 year old compromises appropriate in modern software development environments? Should new software developments be forced to accept such compromises? Is C++ patching old material with new cloth, or pouring new wine into old wineskins? What are we really trying to achieve in programming anyway? Ian Joyner April 1992 |