Taligent architectural goals and the principles that help you design software
Object-oriented design guidelines
Follow these guidelines as you design individual classes.
The client interface reflects precisely the information relevant to the client's problem domain, and no more. Doing this well is key to object-oriented design.
Resources register themselves with services; services should not look for resources. If you need to use another object, let the client give it to you: don't find it yourself.
All interfaces in the Taligent Application Environment are expressed in terms of objects, specifically, classes corresponding to the abstractions that a developer must deal with.
An invariant is an assertion about an object's internal state that is helpful in making sure that the object transitions from one valid state to another, and meets the behavioral promises in its interface.
The C++ typing features are a great help in defining the interface to a class, but the entire definition of a class can't be expressed in the C++ definition. For a class definition to be complete, you must define its constructors and destructor, copy constructor, and assignment operators.
Classes that act as base classes and are not meant to be instantiated are abstract base classes. Follow the techniques in this section to make that clear.
There are two forms of inheritance in C++: type inheritance and implementation inheritance. Either form allows a derived class to share or override behavior inherited from a base class. However, type inheritance allows a derived class to inherit type information as well, allowing for polymorphism.
Use public base classes whenever a collection of classes shares protocol. The only reason for a base class to be public is so that a pointer or reference to the derived class can be converted into a pointer or reference to the base class.
Use private and protected base classes when you want to inherit behavior or override it but don't need to inherit public protocol, such as when inheriting from a framework to override behavior.
The interface between a base class and its derived classes is the contract between the base and derived classes. Design your interface to derived classes so that a derived class that uses every supported aspect of that interface doesn't compromise the integrity of your public interface.
Any function that accepts a reference or pointer to an object of a given class must be prepared to receive a derived class as an actual argument.
If a base class is public, the derived class must correctly implement all aspects of the base class' public interface.
Any public member function of the base class must not have its semantics changed by the derived class, and must accept the same set of arguments. Be especially careful when you have two or more public base classes; make sure that the semantics of all of them are satisfied, particularly if they export the same or similar protocol.
Make sure that lightweight objects you intend to create or destroy quickly do not use deeply nested inheritance or many embedded objects.
Classes in the Taligent Application Environment are partitioned into two categories: base classes that represent fundamental functional objects, and mixin classes that represent optional functionality.
Virtual base classes can be hard to understand. Once you have a pointer to a virtual base, there's no way to convert it back into a pointer to its enclosing class. Virtual bases are always initialized by the most derived class, whether they are accessible to that class or not.
Use multiple occurrences of a base class only when there are data members associated with it.
Design performance into your code from the beginning, and tune your code to improve it.
Choosing the right data structures and algorithms is the most important aspect of good performance. The best way to speed up code is to eliminate it.
You need hard data to solve performance problems. Measure your code to find out what can be improved.
By making your performance tests controlled experiments, you understand which variables change and which are constant. This shows what is effective, and what is not.
Use static objects rather than constant temporaries, but beware of initialization order problems.
A chunky iterator returns multiple data elements at once. The iterator, not the client, determines the count of elements to return based on the internal structure of the data collection, which the client knows nothing about.
Creating and destroying objects can take a lot of time; consider holding onto objects for longer periods and reusing them.
Prepare to rethink your designs as they progress. Don't be afraid to change your design based on experience gained.
A class definition requires a concise human language definition, or you might have a problem.
A class might start life with a concise definition, but over time that definition often becomes fuzzy or nonexistent. Watch the class designs and make sure that an object's role remains well-defined.
Don't put into base classes functionality that all derived classes won't use. If you can't tell whether a function is needed or not, your design is getting out of control.
An interface that is long on state-related functions and short on members that perform an action is a sign of bad design.
Modulitis occurs when member functions don't refer to the
this pointer, either directly or indirectly, or when the class has static members only.
Class definitions reflect the important objects from the client's problem domain, not from the programmer's implementation domain.
Functions must live with the objects that they affect, not in handy packages. Functions that apply to more than one object should usually be static.
Don't let the details about a class' internal implementation leak out through the interface. As more internal details become visible, there is less flexibility to make changes later. Watch for member functions that return a reference or pointer to a data member of the object.
Base classes (especially public ones) should only exist if there is protocol to be inherited.
Push those members that are not meaningful for all of their derived classes down into a derived class; either a concrete class or a less abstract base class.
Avoid functionality in base classes that all derived classes will not use, especially if they have to override it to turn it off.
Use public base classes only when polymorphism is important; private or protected base classes when behavior is going to be inherited; members when behavior is only going to be used.
C++ programming conventions
The standard for the Taligent Application Environment C++ code assumes nothing more than what is defined by the draft ANSI/ISO C++ specification.
Source file conventions are the basic rules for managing and documenting source files when programming in C++.
In order to protect your organization's intellectual property, include a copyright line at the front of every file you create.
If you must read the source code more than once or twice to figure it out, include a comment. Comments complement the source code, not parrot it.
Omit dummy argument names in function declarations only if the meaning is clear without them. Include argument names when you have more than one argument of the same type.
File names should never contain code names. Use straightforward, meaningful names.
Enclose all header file definitions, and all the necessary antecedents, in a
Limit each header file to a single class definition or a set of related class definitions.
Select C++ identifiers carefully. Choose names to enhance readability and comprehension; when a programmer sees a name, it might be out of context. Follow the standard name conventions to make the scope of names explicit. Also, in any name that contains more than one word, the first word follows the convention for the type of the name, and subsequent words follow with the first letter of each word capitalized.
Names should tend to the specific rather than the generic.
The most abstract base class in a hierarchy should have the most generic, abstract name with subclass names denoting refinement. Don't give an abstract base class a name that is derived from a concrete derived class.
Abbreviations are only acceptable when they are consistent and universal.
Routines that allocate storage or take responsibility for storage have special names and guidelines.
Only classes should have names with global scope (that is, not nested within a class). Place ordinary functions and global variables into the scope of their associated class. Most global functions and variables should be static members of some class. The same applies to constant: make them members of an enumeration inside a class, if possible.
Most naming conventions do not sufficiently convey the information that a client or derived class needs to know.
Conventions to follow when designing and using member functions.
Class definitions should always explicitly state the visibility of their members and base classes.
Separate members in a class declaration into sections according to what usually calls them. Place private virtual member functions that are meant to be overridden ahead of public functions that clients and subclasses should not call.
When making a declaration, think about whether you should use an existing type or make a new type to distinguish a new usage.
Declare types rather than using raw C types.
Use a raw C type because it's a dimensionless number and falls within the definition of the C type, or define a typedef based on the function of the type, not its concrete representation.
The only generally acceptable casts are the conversion kind. Avoid all casts involving pointers unless absolutely necessary. Never allow nonpointer casts to silently become coercions.
Assignment operators should return a type that is consistent to the developer, usually a non-
When declaring a typedef of class, place the name between the data type and the member specifications.
Return results by value only when there is no need for polymorphism; use a pointer to return an alias from a function; never return references from functions.
When polymorphism is possible, allow the caller to pass in a variable (via reference) for the result of a function, rather than create and return a result yourself.
 instead of
* for arrays in argument lists, because it is clearer.
Avoid more than one or two default arguments. Further, because default arguments constitute a form of inline declaration, avoid them.
There are very few functions that need (...); use default arguments or function overloading instead.
Use lightweight surrogate-objects to set and get subobjects by value, and to obviate the need for pointers.
C++ handles all types in a program the same way. This is a benefit, but there are some implications for your C++ programming style.
Use pointers when you want multiple references to the same object or a dynamic data structure. Better still, pass the class by value, or use a surrogate, if you can.
Use references when a parameter is to be passed by reference; use pointers when the function you call retains a reference.
Leave storage allocation to the class client. No matter how clever or efficient your storage allocator, it can never be as fast as allocating an object on the stack, or as part of another object.
Design your classes so that using them is like using a primitive type in C.
Don't rely on static objects in other files being available in functions called at static constructor time. Don't count on operations to work at static constructor time unless they are specifically documented to do so, and most should not make that promise.
C++ has features that supersede most of the techniques that required the C preprocessor. Sometimes you need to use the preprocessor to accomplish things you can't do with C++, but the need occurs far less often.
#define for symbolic constants. Instead, use the C++ const storage class.
If your constants define a related set, don't use separate
const definitions. Instead, make your constants an enumerated type.
Declare functions inline to obviate the need for function macros.
Use templates whenever you want to define a family of classes or functions that is specialized for a number of different types.
Some things make your code more readable, some can also make your code more reliable.
goto completely invalidates the high-level structure of the code.
A magic number is any literal written inline rather than defined as a symbol.
Use the built-in Boolean type if you want to keep Boolean flags. Use the C++ built-in bit-field facility for handling single-bit flags.
Arrays with fixed bounds often signal that an arbitrary limit exists in your code. If that limit is exceeded, an exception or possible stack corruption results. If you use large arrays with fixed bounds, consider whether your code is general enough.
Taligent environment programming conventions
Use the Taligent Application Environment library routines rather than the routines defined in the standard ANSI C libraries.
Taligent doesn't permit custom alternatives to the provided Utility classes.
Use the Taligent Toolbox Name Server to name fixed resources; don't use it for naming user-visible entities.
Where possible, use automatic or static allocation instead of your own storage management.
If you must allocate storage, do so in a class, where it is easy to track.
If you must assume that an argument is heap based, document that fact, and if you plan to take responsibility for managing the storage, use the proper naming convention.
Make storage management implications clear to the callers of an interface, especially if the routine allocates storage for which the caller must take responsibility.
Don't allocate more than a few kilobytes at a time on the stack; use the heap for objects larger than a few kilobytes; be wary of arrays as local variables or object fields.
Shared libraries have many advantages. However, because the library is shared among many applications, unreferenced code and data can't be stripped. Static data in particular is a problem.
Avoid modifiable static data in shared libraries, including static objects with constructors. If you need it modifiable, allocate it on demand.
If you use an object as a constant, it's better to create it once and use it repeatedly.
Once your code is released, you should not make changes that break compatibility. Here are some how-to tips and considerations to avoid breaking your code.
You can use nonvirtual functions.
Although you can do this, it won't affect any of the compiled code that calls it. It only affects new callers.
If you refer to a function from an inline, and that inline is called by clients or derived classes, you can never remove the function (with some exceptions).
If you have a class whose definition does not appear in any public header file, you can do anything you want. However, you have to recompile and reship any code that does refer to the class definition.
Member functions cannot be changed between virtual and nonvirtual without breaking callers. If you think you might ever want to override a function, make it virtual.
Private data members can be added, removed, and rearranged only in a few circumstances.
Inline definitions can be very useful, but in general avoid them because they get compiled into your caller's code, making them difficult to revise.
Inline functions can call something else that isn't inline, as long as the other function has identical semantics.
Don't use an inline function definition in a .C file, it is not a portable construct. Instead, define a private inline in the header file.
Sometimes it's acceptable to use inlines if efficiency is extremely important. However, if you do this, you will never be able to change or patch this routine once the code ships.
Don't write the function definition directly in the class declaration.
Use inline functions instead of directly exporting data members as private or protected.
An abstract base class with no storage and no implementation can have special member functions explicitly empty in the class declaration. Don't do this if there are any data members or if there is significant implementation.
Use this technique when the type is not known statically.
Use virtual functions to defer abstract-operation implementation to a derived class, or to allow a derived class to augment the implementation of an operation defined in the base. Do not use virtual functions to trap calls, and then take an action based on where the calls came from.
The class must specify if and how virtual functions can be overridden, and what the responsibilities of the derived class are. The presence of virtual or protected is not enough to define the interface to derived classes.
Any class that might allow polymorphism in the future should use virtual functions now; any function that will eventually allow overriding should be virtual now.
A pure virtual function must be overridden before you can instantiate a concrete class.
A private virtual function can be overridden by derived classes, but can only be called from within the base class. To avoid cascading the calls to inherited functions, define an empty hook function.
Use an Initialize() function to call a special virtual function immediately after constructing a base class instance. Never require the client to call a separate virtual Initialize() function to finish initialization after constructing all bases.
A class must have a virtual destructor if it has any virtual functions, or if it is deleted through a polymorphic pointer.
Switch statements are nature's way of saying that you should be using polymorphism. The same thing applies to lookup tables.
There are several ramifications surrounding making assignment operator=, virtual. This is an area of C++ where there is no single correct approach. Theoretically, the correct approach would be to always make assignment virtual, but doing so leads to problems of its own.
Don't use friend declarations for loosely coupled classes. An alternative is to define internal use only public member functions, and use a comment to denote what they are for.
Use exceptions, not error codes, to deal with unusual circumstances.
Interface specification defines the exceptions that a function can throw. Do not use this technique.
The easiest way to handle resource recovery is to tie it to automatic objects. Many handlers just do the resource recovery and then pass the exception on.
All exceptions generated by the Taligent Application Environment code descend and inherit from TStandardException. Use it.
Signal an exception when a condition occurs that prevents your function from returning its normal result. Don't throw exceptions in destructors, and don't call anything that might throw an exception unless you're prepared to catch it and deal with it.
Recover from an exception only when you can take a sensible action. Also, do not catch TStandardException or (...) and fail to rethrow it, separate error recovery and resource cleanup handlers, and use assertions to signal error conditions due to programming error.
The Taligent Application Environment sometimes stores objects in disk files that are accessed via a hash. In order for the index structures in these files to work when the files are transported across platforms, the hash functions used must return the same result on every platform.
Equality between two objects means that the logical contents of the objects are identical in every respect. As far as the public interfaces are concerned, the two objects always return the same values. The objects can have different internal states that are not captured in an equality comparison, but those are not relevant to the public values of the objects.
For some classes, equality doesn't make sense and you shouldn't create an equality operator. One good test is that where assignment doesn't make sense, equality doesn't either. If you can't define operator=, then you shouldn't define operator==.
If you allow different types to be equal, you must be very careful that the invariant still holds: if X== Y, then Y==X.
Taligent environment programming tips and techniques
Surrogate objects act as stand-ins for other objects. Typically you use a surrogate to manipulate or refer to the master.
An explicit master surrogate is a stand-in for a master object. While you can access the master object directly, you will probably use the surrogate instead.
A handle surrogate is a conduit you use to get to the master object to avoid direct creation or use of the master object. Handle surrogates are similar to counted pointers.
A hidden master surrogate creates and modifies a new copy of the master; the master's existence is transparent to the client. Use hidden masters to lazy evaluate expensive operations.
This kind of surrogate object encapsulates information about an aspect of the master object, but is not necessarily a true surrogate for the master. Instead, it is a synthetic or virtual perspective on that object, and it does not necessarily share a common base class. Iterators associated with the Collection classes are surrogates.
Even though storage management is a design issue, there are some implementation techniques to consider.
If a routine allocates storage that it then hands back to the caller, or if the caller hands it storage that it is then responsible for, name the function appropriately.
Use copy semantics with reference-based implementations.
The most error-prone thing you can do in C or C++ is raw storage manipulation. Do not do it. If you must, use a Collection class. If you have to do it yourself, wrap it in a class. Never include raw storage manipulation in open code.
Many objects have very localized scope and don't need to be allocated on the heap.
Preemptive scheduling causes many pains for concurrency and synchronization. For some relief, synchronize high-level constructs only; avoid synchronizing low-level constructs because synchronization has storage and time penalties.
The Taligent Application Environment uses sychronization locks and surrogates to perform synchronization. Always perform synchronization inside an object, don't let a client do the work.
Memory accesses are not atomic and are not safe to use for synchronization, though multiple threads can safely read a storage location without synchronization as long as no one is trying to change it at the same time.
Any globals (including static class members) that are written or read by more than one thread must be protected by locking.
Avoid sharing memory between tasks. If you have to, modify your shared memory from a server, and give clients read-only access.
If an object allocates storage and you want the object in a shared heap, use a special form of the new operator.
There are several techniques for working with
const functions. Use one listed in this section.
Use destructors for static objects to ensure that a subsystem in a shared library performs some kind of cleanup at application quit time.
These are a collection of miscellaneous programming tips.
Don't create objects in an invalid state and later expect the client to call an open function, or call a close function before destruction. Always allocate needed resources in the constructor.
Use Resurrect to unflatten a flattened polymorphic object.
A common mistake when implementing assignment is to forget to check for self reference--the
this pointer being the same as the argument being assigned.
Include overloaded operators in balanced sets.
When the standard constructor mechanism is too inflexible, use a static member function that calls a private constructor to create a partially valid object, then finish building it and return the result.
When you want to overload constructors, but discover that the argument types you want to use are not sufficient to differentiate those constructors, use a lightweight (inline) nested class to wrap constructor arguments in a distinct type that's easy to overload.
Classes that are used solely by your implementation need not be declared in your public header file, as long as your class refers to them by pointer.
You can use delete p, where p is a nil pointer; because nothing happens. It can be very useful, especially in exception handlers.
If you override an overloaded member function (virtual or not), override all the overloaded variants.
Assign private and protected to special member functions to control access and use of your class.
To write portable code, don't make assumptions about the language or hardware.
There are few safe assumptions you can make about raw C and C++ data types. In general, watch your assumptions carefully, and use typedefs instead of C types.
Bad assumptions make your code nonportable.
Do not use synchronization outside the scope of supported synchronization constructs.
If you write or read any data in a context where it might go to or come from a different CPU running Taligent Application Environment, use TStream and remember that some data types are not portable.
Do not use assembly language.
Follow the steps in this section if you must write nonportable code.
Template implementations are hard to maintain because they get compiled into your client's code. Templates also, by their very nature, tend to bloat the resulting object code. This guide provides design standards and conventions to increase code maintainability, and to reduce the memory footprint.
A class template is the definition of the template for the class; a specialized class is a class produced by invoking the template. By convention, end class-template names with prepositions. Also, place the noninline class-template method implementations in a separate include file.
To be reusable, the implementation class deals with objects at the level common to all types that your template can be instantiated with. Any implementation-hiding class template design depends upon the specifics of your code.
This technique uses private inheritance to share the implementation class between multiple specializations of the template.
An alterative to private inheritance is to delegate the implementation to a member. This technique usually leads to cleaner code than achieved by using private inheritance.
In addition to listing the documents cited in this book, here are other books you can read for further study.
[Contents] [Previous] [Next]
Click the icon to mail questions or corrections about this material to Taligent personnel.
Generated with WebMaker