Managing Dependencies Between Source Files
- Class Declarations and Definitions
- The Preprocessor: For Including Files
- Conclusions
Alan Ezust and Paul Ezust introduce the features of the C preprocessor. They also explain forward class declarations versus including header files, discuss some best practices to reduce dependencies between header files, and explain the difference between strong and weak dependencies.
Class Declarations and Definitions
Bidirectional relationships not very different from Figure 1 appear quite often in classes. To implement them, the compiler needs to have some knowledge of each class before defining the other. At first, we might attempt to #include each header file from the other, as shown in Listing 1.
Figure 1 Bidirectional relationship.
Listing 1 src/circular/badegg/egg.h.
[ . . . . ] #include "chicken.h" class Egg { public: Chicken* getParent(); }; [ . . . . ]
The problem becomes clear when we look at Listing 2, which analogously includes egg.h.
Listing 2 src/circular/badegg/chicken.h.
[ . . . . ] #include "egg.h" class Chicken { public: Egg* layEgg(); }; [ . . . . ]
The preprocessor can’t deal with such circular dependencies. In fact, neither header file needed to include the other. In each case, doing so created an unnecessary strong dependency between header files.
Lucky for us, we can use forward class declarations instead of including headers. A forward class declaration serves a purpose similar to that of a function declaration: It allows us to refer to a symbol without having its full definition available (see Listing 3, line numbers added). Declared classes can be used as types for pointers or references, as long as they’re not dereferenced in the file.
Listing 3 src/circular/goodegg/egg.h.
1. [ . . . . ] 2. class Chicken; 3. class Egg { 4. public: 5. Chicken* getParent(); 6. }; 7. [ . . . . ]
- Line 2: Forward class declaration.
- Line 5: Okay in declarations if they’re pointers.
We can define the getParent() in the code module, egg.cpp, shown in Listing 4 (line numbers added). Notice that the .cpp file can include both header files without causing a circular dependency between them. The .cpp file has a strong dependency on both headers, while the header files have no dependency on each other.
Listing 4 src/circular/goodegg/egg.cpp.
1. #include "chicken.h" 2. #include "egg.h" 3. 4. Chicken* Egg::getParent() { 5. return new Chicken(); 6. }
- Line 5: Requires definition of Chicken.
Forward class declarations make it possible to define circular bidirectional relationships, such as the one above. We pushed down the dependencies on headers into the source code modules that actually needed them.
In Java, it’s possible to create circular strong bidirectional dependencies between two (or more) classes. In other words, each class can import and use (dereference references to) the other. This kind of circular dependency makes both classes much more difficult to maintain, since changes in each can break the other. This is one situation in which C++ protects the programmer better than Java does—it’s impossible to create such a relationship accidentally.
Java also offers a forward class declaration, but it’s rarely used, since Java has no separate header file and implementation file.