Notes for week 9

For this week's links click here.

Problems of Large Projects

Every design paradigm is tested to its fullest with a large project. As the number of lines of code in a project grow linearly, the control flow paths often grow exponentially. This is true of object-oriented systems as well as standard programs. In addition, object-oriented programmers often over-develop classes to meet requests from many different users. This leads to a very large number of class functions and if the linker is not efficient very large executable files. Other problems like poor design of the sequence of includes will often lead to very long compile times and this frequently leads to a reluctance to redesign foundation classes. The book Large-Scale C++ Software Design by Lakos covers many of the aspects of the physical design of large programs. We will consider just a few here.

Include Guards

Multiple reads of header files is one of the causes of long compile times with large projects. The simplest way to avoid these re-reads is to use include guards and redundant include guards. You have been using them since last semester, but this is a good time to review their use.

Whenever you have created headers, you have bracketed the header code by the complier instructions:

#ifndef FILENAME_H
#define FILENAME_H
//code here.
#endif
These include guards prevent the recompiling of headers that have already been compiled. The main problem with include guards is that the header is that if the include statement doesn't incorporate redundant include guards the header file will be openned every time an include statement is encountered. Even though the file is not recompiled, it still takes time to open. We have used redundant include guards by testing the existence of the defined identifier before executing the include. Our include would look like this:
#ifndef FILENAME_H
#include "filename.h"
#endif
By now most of you have seen that AppExpert creates code guards, but does not create redundant code guards. This means that you should add the redundant guards to your code.

Often we use headers provided by libraries. These headers may or may not have include guards. Even when they have include guards, these guards may be hard to remember since everyone seems to have their own form for such guards. To overcome the lack of guards or unify the form guards take there is an alternate form of include guard.

#ifndef FILENNAME_H
#define FILENAME_H
#include 
#endif

By defining the include guard at the point of the include you can insure all of your included files have include guards and you control the form these guards will take.

Separate Used in Interface From Used in Implementation

One way to cut the number times a file is included is to make sure that any files included in a header is necessary for the definition of the classes in that header. If for example you define a class that has a function that uses a stack, before including stack.h in the header for that class make sure that the stack is part of the definition of the class. If the stack is only used in member functions, then it should only be included in the cpp file for the class.

The reason for this separation is clear if you have ever watched the sequence of includes for a windows compile. If you alter something in a header, all of the files that use that header have to be recompiled. On the other hand if you alter something in the cpp file, only that cpp file is recompiled. If the stack header is altered, only the cpp file that includes the stack will be changed. Whereas if stack were included in the header, ever file using this header would be recompiled. This is also a good reason for not putting any but the most trivial function definitions in the header file.

Defining Constants And Enumerated Types

C programmers frequently use preprocessor constants to define constants

#define GOOD = 0
for example. This leads to many problems in a large program since 0 is substituted for GOOD whereever it appears in code. If a programmer later defines the following enumerated type
enum (BAD = -1, GOOD = 0} 
the compiler sees this as
enum (BAD = -1, 0 = 0}
A second attempt to define GOOD might be the c++ method
const int GOOD = 0;

While this overcomes the substitution problem above, it creates name space problems. One could easily think of a case where another file in the same project might define GOOD as 1. This would not only lead to probable compiler errors, it could lead to a long search for the other definition of GOOD.

The best way to define constants is to associate them with the class that uses them. This can be done in two ways. First integral constants can be defined as enumerated types in the class. For example:

class MyClass
{
public:
    enum {GOOD = 0}
}
Now this occurance of GOOD can only be referred to as MyClass::GOOD. There will be no conflict with GOOD defined in class YourClass, since it will always be referred to as YourClass::GOOD. In addition we need only look for the header defining MyClass to see what the value of MyClass::GOOD is.

Not all constants are integral. We could have a double or a string associated with a class. These cannot be stored in enumerated types. Instead we can use static constants.

class MyContainer
{
public:
    static const double DEFAULT_VALUE;
    static const string CLASS_NAME;
}
The values for these static constants are assigned in the cpp file for this class. Recall that there is only one instance of a static class variable or constant. This instance is shared by all members of the class.

One general point about nesting constants. Most constants are only used in the context of a given class, so can be made private members of these class. Even those constants used outside the class are used in a very limited context, so the number of times the const's name with scope designation is used are very small in the context of a large program.

Compartmentalizing Classes - The Facade

The problems we have considered up until this point are concerned with the relationships between files within a project. When projects grow large enough to make separate compiling desirable (read dll), we want to organize sections of the program in such a way that the section itself has an interface. With dlls, we can accomplish this by controling the number of classes defined as export classes. In the following case study we will create a dll that contains numerous descendents of a parent class. The main program will incorporate the classes as a dll, but will only know the name of the parent class. This will allow the users to add different classes to the hierarchy without changing the main program.

Create a DLL

You can create a dll by following the steps laid out in the document you get by clicking here .