This appendix provides guidelines for what to do and what not to do when using C++ in your ACCESS Linux Platform applications. Because the Platform imposes size limitations, and because you need to take performance considerations into account, you cannot simply implement typical object-oriented designs as-is. C++ can be just as efficient as C, and easier to maintain, if you follow the guidelines outlined in this appendix.
One general piece of advice: if you are new to C++, make sure you recruit an experienced mentor to do code review and discuss design plans. As with any language, C++ takes careful consideration when writing efficient and clean code using the available features.
Libraries
Standard C++ Libraries
The standard libc++.so (needed for such things as operator new() and std::string implementations) is rather large, weighing in at over 800k, so ACCESS Linux Platform includes a slimmed down version which is less than 200k that you should use instead: libalp_stdc++.so (in opt/alp/lib/).
Disallowed C++ Features
In order to slim down the standard C++ library (libalp_stdc++.so), certain features had to be eliminated. The ACCESS Linux Platform C++ library does not support:
Virtual Methods
There aren't any hard and fast rules for deciding when a method need to be virtual. There are, however, a few simple questions you can ask yourself that will help you decide.
- Is the method a "getter" or "setter"?
- Are all other methods in the class non-virtual?
- Is the method inline?
- Is the method private?
If the answer to any of these questions is yes, then odds are good that the new method can be non-virtual. In addition, it is important to keep in mind that, while deep object hierarchies are a natural part of C++ programming frameworks, the reserved space for virtuals can take up more space than you might expect. We recommend keeping virtuals and object derivatives to an absolute minimum.
Constructors
Every class needs a minimum of two constructors: the "default" and the "copy" constructor. The default constructor for class CFoo has no parameters, and the copy constructor for CFoo is of the form CFoo(const CFoo&). Be sure you provide both even if there is only an empty implementation. The reason for this is twofold:
- If you haven't defined a default or copy constructor, one will be provided for you, and you may not like the one you get.
- You need to make an explicit choice about whether either or both of these constructors can be called by clients. If they can't, then you need to define them as private.
The copy constructor and assignment operator must be protected against operating on themselves by using the following technique:
const PMError& PMError::operator=(const PMError& anError) {
if (this != &anError) // Prevent copying itself
{
itsError = anError.itsError;
}
return *this;
}
Constructors must never fail or throw exceptions. If the object requires sophisticated initialization it must provide an Init() function. This is better because in throwing an exception the constructor may leave the object in an undefined state (invalid pointer value, open file, etc.) and then the developer will have to use another method to check the object integrity. The Init() function will be less costly in the long run.
Destructors
If you have any virtual methods in your class, make sure your destructor is also virtual. Destructors must never throw exceptions because the object might be left improperly deleted, and if this destructor is called as part of another exception handling, it will abort the application by throwing an exception itself.
Assignment
C++ compilers treat assignment just like constructors: if you haven't defined one, the compiler will create one for you. This is definitely a bad thing if your class has allocated any dynamic memory or consumed any other kind of shared resource. And as in the case of constructors, if you don't want to support assignment you need to make your assignment operator private.
Abstract Classes
An abstract class is a class with at least one pure virtual method. A pure virtual method is one of the form:
HandleSpecialEvent() = 0
An abstract class can not itself be instantiated, however you can instantiate a derived class that overrides the pure virtual methods of the abstract class.
Abstract base classes are an excellent way of defining a protocol independent of implementation.
Friends
Avoid friends like the plague. They break data encapsulation, and an excess of friends is typically the result of a design that could be improved. There are, however, exceptions to this rule.
There are times when you must have friends. For example, the only way to overload operators that behave symmetrically is to use friend functions.
Public or Private Data Members
The only time data members ought to be public is when they are part of a truly primitive type, such as CPoint.fX and CPoint.fY. Data members ought to be private in virtually all other cases. There are three primary reasons for this rule.
- If accessing the member is truly a performance bottleneck, make the accessors inline. However, experience shows that this is seldom a real bottleneck.
- Since one of the primary advantages of object-oriented programming is supposed to be data encapsulation, why erode this benefit by making your data visible? You gain a significant amount of flexibility in your design and implementation by taking the little extra time required to provide accessor functions for client access to data.
- Accessor functions (getters and setters) allow you enforce client semantics that are not possible with direct access to members. For example, a getter can enforce const semantics whereas it is not possible for a data member to be const for clients and non-const for the base class itself.
Limit Default Arguments
Default arguments are useful if used in moderation. A parameter set that includes several default parameters is usually a sign of parameter creep.
One of the problems associated with multiple default parameters is that it tends to force the caller to explicitly specify parameters for which they have inadequate knowledge. For example, suppose you need to call a method that has two default parameters and you only have the appropriate data for the second default parameter. What value do you give for the first default parameter in this situation?
Another cost of using default parameters is that their default values are specified in header files. Using overloaded methods allows you to specify default values within .cpp files.
Last, but not least, default parameters provide yet another example of how to create ambiguous methods. For example, consider the following three method definitions.
CFoo();
CFoo(int i = pi, char c = 5);
CFoo(int j);
Which method will be called when you write CFoo()? CFoo(27)?
Pass By Reference, Not Value
Passing parameters by value is one of the most common sources of performance problems in C++. Not only do you have the overhead of moving the class data around, you have the potentially large (very, very large) overhead of moving all the class's base class data around plus the cost of executing all the associated constructors, and methods called by the constructors, destructors, and the like.
There is a way to provide pass-by-value semantics without the attendant runtime overhead, and that is to use reference-counted classes. Not all classes are candidates for this approach because of the increased complexity of the underlying class implementation. However, when you are trying to mimic a base type such as CString, the gain in conceptual simplicity is worth the extra implementation overhead.
Pass Collections When Possible
One of the better examples of passing a parameter by reference is when the parameter represents a collection of data. Passing the collection instead of creating one and passing it back gives more flexibility and eliminates a potential source of memory leaks. This is also preferable to returning a class as a function result, which has all the same performance problems as passing parameters by value.
Avoid Returning References As Function Results
There are several potential pitfalls associated with returning references as function results. Although most of the underlying issues are the same as returning pointers, references tend to be more insidious due to the fact that there is no syntactic flag to highlight the use of what amounts to an alias.
Lifetime Of An Object
The first, obvious question you must be able to answer when returning a reference is whether you can guarantee that the underlying object will outlive the reference. This is not always as simple as it would seem. In particular, there are many cases when temporary objects are created and their destruction is compiler dependent.
An Acceptable Case
One way to safely return a reference as a function result is when the object to which the reference refers was passed as a parameter (as a reference, of course).
Use of Const
The use of const data members is preferred to #define for two reasons: the namespace is constrained, and they have types associated with them. For example, if you accidentally redefine a name with a #define the compiler will silently change the meaning of your program. With a const, you'll get an error message.
In addition, const data members provide an explicit reminder of the immutable nature of parameters that will not be changed by a method.
Enums
Prefer enums to constants. Each enum is treated as a separate type for the purposes of type checking. It also explicitly groups related constants. Unfortunately, enums can not be used where portability is an issue due to their platform and compiler dependent size.
Avoid Macros
With the inline and const features of C++, there is almost no reason to use macros. As with all #define constructs, you can significantly change the meaning of your program without any error messages being generated.
Avoid Casts
Frequent casts are almost always an indication of an improperly thought-through design.
There are three different kinds of casts in C++. The first kind is where the cast changes an object from one type to another. This includes casts between built-in arithmetic types and casts involving classes (not pointers to classes) related by inheritance. These are usually safe because an actual conversion is taking place.
The second kind is a cast that involves pointers and type coercion. This is the killer. The bit pattern of one type is interpreted as another type.
The third kind is a runtime-checked cast. Since ACCESS Linux Platform doesn't include support for RTTI, you can't use the C++ dynamic cast feature.
Hide Storage Allocation
Various research reports have stated that memory management-related bugs account for between 30% and 60% of all bugs in C++ applications. Given that memory management bugs also tend to be particularly difficult to track down, every bit of attention that can be given to minimizing the potential for these bugs will pay off big-time.
Avoid Allocating Dynamic Memory
You can often avoid dynamically allocating variables from a memory manager by using stack-based variables. One case where this is not possible is when you need to pass ownership of an object with a variable type. Any time a method must determine the type of the object to return, the method must allocate the object, not the caller.
Hide Allocation Inside a Class
If you have to allocate storage, do so inside a class where it is easy to track. Another advantage is that it is possible to use special tricks, like using surrogates for reference counting and lazy allocation, without having to modify the class's clients.
Use a Pointer Class Wherever Possible
A pointer class is a type of smart pointer that acts like a pointer to an object, smells like a pointer to an object, and most importantly handles deallocating the associated resources when the object is destroyed or a new pointer is assigned. This class can be implemented either as a template or non-template class. A good discussion of smart pointers can be found in More Effective C++ by Scott Meyers.
Use Global Names Only For Classes
Scope names to classes. Although this means you have to type more characters, it really helps identify the context for a variable or constant and virtually eliminates name conflicts between libraries. For example, instead of the constant kRed you might use CFlower::kRed.
Design Guidelines
Composition Versus Inheritance
Composition has never really enjoyed the positive attention it deserves. There are many instances where significant simplification and code reuse and result from choosing composition over inheritance for particular design patterns. One of the best ways to illustrate this point is with a real-world example.
Consider a design for a graphical or outline display of hierarchical data, where each node in the hierarchy can have an icon, or a text field.
Now suppose we needed to add two more types of label: a label that had an icon and a text field, and another label that had one icon and three text fields. Although you could embellish the CIconLabel or CTextLabel class to handle the behaviors required for the extra icons and text fields, that approach would quickly become untenable. Every different type of label would require yet another modification to the same class to add the new behavior, and even more code to insure that you bypassed the code that wasn't currently required. In the extreme case, why use inheritance at all?
The other approach, using inheritance, is unfortunately not that much better in this case. Plain inheritance is not well matched to design problems that are fundamentally combinatorial in nature. Another drawback of this approach is that it is relatively static. Even though client code can choose which class to instantiate at runtime, the choices available have been predetermined at compile time.
Read-Only Vs. Semantic Const
The interpretation of const in C++ is a matter of confusion. The language defines const to mean that the representation of an object does not change. Many people argue that this violates the data abstraction principle that is so important to object-oriented programming — clients shouldn't care if the representation changes, only if the semantic state of the object changes. This is an important point because, for example, a class can have an internal cache that changes on a call to a member function, but doesn't change the semantic state. Should that method be const or not?
Unfortunately, there are cases where the stricter interpretation is required. For example, when placing objects in a shared area that only has read access, or when worrying about concurrency (acquiring a shared rather than exclusive lock).
There is no simple answer. For most cases, the less strict, semantic const interpretation is sufficient, however care needs to be taken when dealing with cases that require guaranteed immutability.










