The C++ programming language paradigm provides two mechanisms which can be used to inform one entity about the existence of another. These are (a) inclusion of the
header file containing the declaration of the former entity, and (b) forward declaration. There is a dispute among programmers as to which of these two is the better
approach. In this article I take the position that inclusion of the header file is the correct approach and explain why. Nevertheless there are cases when forward
declaration is admissible and in fact the only possibility.
Perhaps you might have heard the proverbial story about the developer who started developing a dog house which was very successful, so he kept adding to it. Unfortunately
it couldn’t quite make it to becoming a skyscraper because it collapsed under its own weight. This anecdote suggests that the essence of the software is its architecture.
For the purpose of this article I will assume that this is a self-evident truth. However, for a larger discussion see my other articles on the subject. Provided that this
premise is true, it follows that it does not really matter if the software is compiled with Visual Studio or another compiler or if the compilation took 1 minute or 1 hour,
or whether the compiled code is slightly smaller or larger, faster or slower, etc. If the architecture is wrong then nothing else really matters. That is not necessarily true
if the project is sufficiently small, i.e. if the developer we mentioned earlier stopped any further development as soon as he finished the dog house, or if his dog doesn’t
really mind the cold wind in the winter. However if the architecture is sufficiently poor then even a doghouse will collapse under its own weight. I will assume this
foundational premise, which is that any code must be as architecturally sound as possible, and will continue with the problem we are concerned with in this paper.
In the C/C++ languages, types are described in pairs of files. The first file in each pair is the header file or .h file and is intended to be the placeholder of the declaration
of the type. The second file is the implementation or .cpp file and is used to define the actual operation of the type. We formalize the following definition.
From this definition follows the obvious corollary:
A type is completely defined using a pair of files:
- The declaration (.h) file contains the blueprint of one type (class).
- The implementation (.cpp) file contains the function of the type declared in the .h file.
For consistency and unity the names of the files match the name of the defined in/by them type.
There is a one to one correspondence between a type and its .h/.cpp pair of files.
- The type is completely defined in the pair of files.
- The pair of files contains no other declarations except of the type they define.
Note 1: Due to relaxed rules in the compiler implementations, the border between .h and .cpp files is often fuzzy since definitions are often placed in the header files and vice versa.
Further, due to undue limitations of the C++ language, such as the lack of local functions, declaring of local classes is common and often used to overtake the lack of local functions.
Further, for small types the overhead to provide pairs of files is too large and developers prefer to contain such small entities in a single place as well as many of them.
Note 2: Due to issues with templates they are typically defined only in .h files.
Note 3: In the C programming language paradigm, there are no classes, but only structures. Functions are however often separated in .h/.c files
where the .h file contains the declaration (prototype) of the function and the .c file contains its definition, i.e. body.
Definition 3: Forward declaration is a two word specification in the form class [Name]; or
struct [Name]; - where [Name] is the name of the forward declared type.
From this definition it clear that forward declaration only informs the compiler for the possible/assumed existence of a type with that particular name. The
proponents of forward declaration typically cite the following advantages:
- Faster compilation.
- Total encapsulation in terms of hiding of everything about the declared class.
However, from our foundational premise which we established in the beginning it follows that:
- Faster compilation is entirely irrelevant to the quality of code and thus is dismissed.
- "Total encapsulation" – while encapsulation of data is one of the most important concepts when discussing architectural design,
the above technique is not at all encapsulation but is a "because" and in essence declaring a void pointer to be later interpreted.
- On a humorous note, this kind of "total encapsulation" reminds one about the misfortunate Earnest T. Brass who instead of
stealing the beautiful Charlene stole the "totally" encapsulated (totally wrapped up in a marriage gown) Barney Fife.
Although to the careful reader these reasons are sufficient to dismiss Forward Declaration as a very poor practice to be always avoided unless necessary, we will
continue with more examples and reasoning.
The Forward Declaration proponents insist on always using it except in the following cases. Forward declaration should not be used when one needs to:
- Access a property or method of the class or any of its ancestors.
- Use pointer arithmetic (Note of the author: this is not always true).
- Use of the sizeof operator, or mem-copy of object.
- Use of Real Time Type information.
- Construction or destruction.
- Any reference to an object of this type.
- Derive a class from the forward declared type.
It must be noted that these are not mere suggestions, as its
proponents seem to suggest. In fact these are the cases when the use of Forward Declaration leads to compilation errors and simply cannot be used. In
summary, forward declaration cannot be used whenever an object of this type is used in whatsoever meaningful way, other than "An unidentified type
called X may exist." This leads to the following informal proposition:
A Forward Declared type is a "UFO" type. ("UFO" stands for the usual "Unidentified (Flying) Object")
Notation 5: Forward Declared
- The meta object (forward declared type) may or may not exist.
- If the meta object (forward declared type) exists, it has an entirely unknown nature.
- Based on the used keyword class or struct,
one could invoke very limited, and generalized speculations on its nature, provided that the type exists in the first place.
type can be interchangeably called a UFO
Motivation: Follows immediately from Proposition 4.
Note 1: Thus it is fair to say that using forward declaration turns computer science into speculative broth.
Note 2: For completeness, we will mention that the alternative to C/C++ approach used in
languages such as C# is to have only one file in which types are defined entirely. In this case there is no separation between declaration and definition – in
fact there is no declaration at all, so that the entire content of the defined type is placed in one single file. The compiler/interpreter parses the files first
to collect all declarative information it needs before compilation.
We continue with various arguments against using UFO types (forward declared types).
By definition, the .h file is the interface file of the type, using .h files:
- Helps to maintain meaningful the relations and dependencies between types and objects, i.e. stimulates well-constructed code.
- Further stimulates better quality of code:
- Disallows circular declarations – circular definitions in a broader sense are a common mistake when definitions are not made carefully. The use if .h
files in a circular manner leads to infinite regression during compilation, and is immediately noticed. Conversely, forward declaration does not and
intrinsically cannot detect them. Thus the use of .h files is the preferred approach.
- Placing code in .h files increases the compilation time – thus using .h files stimulates correct placement of code in .cpp
files (instead of .h files), thus maintaining the .h file as the interface file and .cpp file as the definition file. It also stimulates minimal inclusion
of headers, preventing redundant declarations and inclusions and thus stimulates better understanding of the system, as well as creating well-structured
- Exposes the interface of the class – note that this was one of the supposed advantages of the forward declaration. The
definition of interface in Computer Science is a "Point of interaction between two components." Hence, the .h file inclusion publishes those methods
and properties which are made available to access by the type, which is precisely the objective of an interface. In difference, the private methods
and properties are known only to the "client" developer, but they exists in at least 2 meta-levels above any instantiated objects, so that they can
consider the architectural consistency of the model they build, whilst the private properties and methods are entirely hidden to any types and (potential)
objects which might try to use them. The same optimal exposure/concealment applies to protected methods and properties.
Using forward declaration to hide properties has the following implications:
- Hides everything including the public methods and properties of the type, but this is a violation of the concept of access rights in type declarations.
- It prohibits the use of public nested in the forward declared type declarations.
- Since Forward Declaration hides the properties of the forward declared type entirely, it stimulates using "public" access control
declaration everywhere in those types. The latter significantly deteriorates the quality of code.
- As a consequence of the above, .h file inclusion keeps the attention of developers in high alert and requires them to produce better than otherwise quality of code.
- Inclusion of .h files leads to easier to understand code and system.
Argument B: An argument has been made in favor of Forward Declaration, in particular for cases when one organization provides code to
another. In this respect it is needed to make a clear distinction between software designed for internal consumption and software designed for use by
external organizations, where the software provides service and is not provided as "Know How".
Clearly, this argument is ill aimed, but even if so, when providing services using interfaces in COM sense, interfaces in sense of .h files containing library
function prototypes, and handles are the optimal options, with no viable alternative. Clearly, to use either, one needs to include the .h files provided by the
vendor. Forward declared types in this sense is no better than ludicrous.
Argument C: Some have argued that using forward declaration should be used when the respective type is used only for parameters in
A method part of a type implements code for doing a particular piece of work, and is using the set parameters passed to it. Any method should be well defined,
including its purpose, parameters and results at the point of its declaration (.h file). This requires that the architect of the method declaration must know
the interface and nature of each passed parameter (object) in order to ensure that it is fit for purpose.
For example: what data and services it can receive from it, as well as what parameters it requires and what exceptions it possibly throws.
However, if forward declared, the parameter type is not known at all. Not even if it is instantiable, much less its properties. This leads us to conclude that
the author of the method cannot guarantee the integrity of its definition, and hence the integrity of the type they are constructing. Therefore the use of Forward
Declaration in this case is contrary to the designing of well-defined and consistent code.
The use of Forward Declaration implies that the software systems development must be code driven, i.e. write the code in the .cpp file, and then declare the finished
method in the .h file, as opposed to architecture driven. However, this was exactly the proverbial dog-house building software approach.
Argument D: Argument D: By definition, header files are declaration containers representing the declaration of the type. A declaration must
be well defined, that is consistent i.e. Complete and Non self-contradictory. However, a forward declaration is entirely incomplete, and can by no means guarantee
to be non-self-contradictory. Hence, the use of forward declaration is inconsistent, and therefore must be avoided.
For example, it is clear that a forward declaration is entirely incomplete. Suppose someone else implements a type which we forward declare. We use stubs in
order to progress our work, expecting that they will deliver on their promise. However, they may find that they cannot deliver the promised type, because it
is impossible to implement. Unless we are able to remove the forward declared type entirely, our work becomes useless, which may still be the case even if
we are able to remove the forward declared type.
Argument E: Forward declaration can be only used if all pointers have the same size. Although this is usually the case, it is neither
mandatory nor necessary. If pointers to objects have different size then forward declaration cannot be used unless one instructs the compiler for the size
of the pointer to that object. However, if the size of the pointer to an object of forward declared type is dynamic then forward declaration becomes entirely
impossible except for purely declarative statements, such as reserving the name of a type and friend declaration. Even though, typically pointers have the same
size, this argument shows that forward declaration is fundamentally inconsistent, except for purely declarative statements, such as reserving the name of a type
and friend declaration
Argument F: Suppose that we define an abstract class which is a "pure" interface type containing only public abstract virtual methods.
Forward declaration on such a type (interface) is equally legitimate as forward declaration on any other type, but it is clear that in this case it entirely
defeats the purpose of the type. In fact forward declaration in this case can be classified as nonsense.
Argument G: Forward declaration has semantics of Pointer to unknown type with known name. Thus it differs from a void* only by the known name.
Thus if we extend the void* semantics to a [void*, const unsigned long] pair, the second element of which is used as differentiator to make appropriate casting of
the void*, e.g. in a switch statement. The latter approach defies not only the Object Oriented Paradigm, but even very basic good programming techniques, and is
clearly unwanted. However, the difference between it and the forward declaration is only cosmetic.
Forward declaration is a tool necessary in cases such as the following:
- Declare friend type.
- Reserve a name for a type.
- Allow implementation of mutually dependent entities. For example definition of mutually dependent interfaces, in which there are methods returning pointer to the other interface.
- Allow implementation of dependent types placed in the same file.
However, forward declaration has been abused, especially by the Unix community, using it instead of interface .h files. Perhaps the reason for this is that
some developers are not willing to do what is necessary to maintain a well-structured hierarchy of types together with their .h/.cpp files. Others may be
not fully appreciating the principles of Consistency and Object Oriented Programming. Yet third, might be simply lazy and/or sloppy.
The proponents of forward declaration site faster compilation as main advantage. However, slow compilation due to .h file inclusion, is (a) irrelevant to the
quality of the system, b – suggests that there is poor programming including definitions in the h files (wrong places), multiple inclusions, unnecessary
inclusions, while .h file inclusions must reflect the relationships, including inheritance, between types.