In our daily experience, we are making our project in different files, not in a single file. We will do this because we want to reduce the compilation time during the development as well as reuse the code written in different files. For example, let’s say that you want to make some project that has 10,000 lines of code. During the development of project, or after it, if we change any single line, the compiler has to recompile all 10,000 lines. With today’s computers, it might not be a big problem, but it will eventually become a nightmare when projects become larger. On the other hand, if we split our project into more than one file, such as 10 files each containing 1000 lines, any change in one file ideally should not affect in other files.
During the development of a project, we usually talk about the design of classes, discussing in terms of design pattern, and describe the relationship among the classes. But, most of the time we are not concerned about the files in which those classes are written. In any large-scale project, it is not also worth the time and energy to study the physical design of the project, but in some cases, where the project size is very huge, it is inevitable.
It is very common that a large-scale project has some general-purpose classes that are useful in other projects too. So, a natural solution to use those classes in other projects is to make those classes in separate files. It is a common practice of C++ users to make two files for one class; one file contains definitions, and the other has the implementation of the class and its member functions. A Point class will be something like this:
But, if you didn’t program carefully, sometimes it is not possible to just include these two files in another project and use it. One of the most common problems that may arise is to include some other definition files in your project that are not needed. And, other files also may need some other files, so at the end you may have to include a bunch of files to just use one single class.
One example of this is that you might want to use a Database class that is created in some library or DLL; then you also might need to include the definition files of some other classes in that library, such as RecordSet.H, DBFactory.H and DBException.H. The situation is even worse if you have to include the definition files of different Database classes, such as OralceInterface.H, SQLInterface.H, and SybaseInterface.H.
It is better to see carefully which files are included in files, especially which files are included in Definition file (Header file). If you change anything in any definition file, all the files, whether it is a Definition file or Implementation file, need to recompile. From the compiler’s prospective, a CPP file with all preprocessor expended is called a translation unit. In other words, a translation unit is an Implementation file with all the definition files included. Here is an example of a translation unit.
Now, if you change anything in any of the definition files that are included in Camera.CPP you have changed this translation unit and it has to be recompiled. The situation becomes more serious if these definition files are included in more than one translation unit; the change in one definition file means that you need to recompile all those translation units.
Changes in definition files can be minimized if we use them only for definition, not for implementation.
“In other words, implementation of a function should not be in the header file even if it is only a one-line function. If performance is concerned, that function can be declared inline explicitly. If there is any change in the implementation of the function only, the compiler will recompile only that translation unit.”
However, in the other case, the change of implementation of function means recompiling all translation units that have this header file.
If one header file is included in another header file, changing the first header file will change all the files that include either the first or second file. The situation becomes even worse when the header file includes another header file, which includes another header file, and so on. Now, a change in one file may mean that the compile is not limited to one file only, but it may involve recompiling the whole project. This diagram shows this concept clearly.
It doesn’t matter that your Camera class does not include Point.H or ViewPort.H directly; it is included in the Camera translation unit. Now, a change in Point header file will compile not only Camera translation unit, but also all translation units in this example.
The basic rule of thumb to minimize physical dependencies is:
“Try to avoid inclusion of a header file within a header file until you don’t have any other option.”
But how can we make the compiler happy when we are not including a header file? To see the answer to this question, we first must understand in which cases we are forced to include a header file and in which cases we can avoid it.
You have to include the header file when you need the full details of the class. In other words, you have to include a header file when you are accessing a member function or variable of a class, inherit from a class, or aggregate its object in another class. We have already decided not to write implementation code in a header file, so this first case will be eliminated automatically. If you use another object in member functions only, either creating its local object or use it as a parameter, or it contains a pointer of another class, you do not need to include its header file. To make the compiler happy too, you can just do forward of that class in the header file. Now, we can restate our basic rule to minimize physical dependencies:
“Use a Forward declaration instead of including a header file wherever possible, such as in the case when you are not inheriting a class or aggregating it in another class.”
For example, in this case we have to include the Point header file in the ViewPort header file.
// Other functions
// other attributes
But, there is no need to include a ViewPort header file in the Transformation header files if it just uses it. Such as the following:
void Scale(int p_x, int p_y, ViewPort& p_viewPort);
// other functions
But, you have to include the ViewPort header file in the Transformation implementation file because there is no way to avoid this. The situation is a little bit better, and now a change in Point.H will not propagate in all translation units. At least, it will not have any effect on all the translation units that include the Transformation.H file.
You can further reduce the physical dependencies by making a pointer of a class rather than making the object of a class. In the case of a pointer, the compiler does not need full details in header file and it can be totally eliminated.
In this case, you have to create and destroy the object yourself, as well as there is extra overhead of function calling. In addition, this physical design might not fit very well to your logical design because you are not doing inheritance; therefore, you can’t access the protected data of a class, and cannot override virtual functions. This technique is also known as the “Pointer to Implementation Principle” or, in short, the “PImpl Principle.”
There might be one solution to avoid including a header within a header. Include all header files in the CPP file before including its own header file. Take a look at the above example; ViewPort.H needs the Point.H file. Now, include this header file in ViewPort.CPP before including the ViewPort.H file.
Compilers will look at this translation unit so that it seems to be something like this:
and happily compile this unit. But, there are two problems in this approach. The first is that you have to include header files in the proper order. You have to remember the dependencies of the header file and include it in the proper order or the program will not recompile even if you include all the required header files, but not in proper order. The second problem is even more problematic; if you want to use ViewPort.H in any other translation unit, that translation unit will not compile until you include Point.H. From a physical point of view, you haven’t changed anything, but you also create more problems by introducing dependencies among header files; these are hard to remember. Here is one more rule of thumb for managing physical dependencies:
“Never make any files that are dependent on the order of header file inclusion.”