#include directive implements inheritance

I came to realisation that certain design principles originally formulated within object-oriented programming (OOP) are applicable to a wider range of programming concepts. Inheritance is one of them.

In OOP, inheritance is a type of relation between two (or more) classes when you declare one class to be everything that another class is, and possibly something more on top of that.

Inheritance can be useful for many purposes. Abusing it can also lead to creation of serious maintenance problems, with classes being connected too tightly and in a wrong manner.

But the idea of “Y is everything that X is, plus more” can also be found elsewhere. The preprocessing directive #include found in C and C++ exhibits very similar properties.

Include

The basic operation of an #include directive is rather simple: when encountered, contents of the referenced file is pasted unmodified in-place of the directive. There are certain additional effects of using it that are irrelevant for this discussion.

The canonical use case

The classic and well established idea implemented via #include is exporting interfaces (names and types) of individual compilation units (source files which actually define behavior).

But literal inclusion of text is a much more powerful and therefore potentially dangerous technique. Nothing prevents implementing other types of abstractions through #include, and there are few controls in the languages that stop from misusing the directive.

Problems with #include

Let’s limit the discussion to the “intended” use case of defining interfaces. Even within its limitations, there is a great potential for design mistakes and maintenance problems attributed to the headers.

Depending on unrelated things

Headers often include other headers for things that they need, most often for type definitions found in other headers. The problem is that it is “all or nothing” process; you cannot import only specific symbols by using #include.

With overly big headers and overly long include chains, this leads to the situation when source files start depending on things they do not really use. When compiling the project, this leads to recompilations for all files that include a given touched header even when they do not really use anything from that header.

Situations of breakage caused by changes deeply within the include chain which, after deeper inspection, do not really affect the code depending on it, become more likely.

These are all symptoms of violating the Interface Segregation Principle: Do not depend on what you do not use.

This situation is very similar to how fragile object-oriented code becomes when class hierarchies become very long: classes are forced to depend on things they do not really use, but which are included in parent classes “by accident”.

A solution is similar in both cases: strive to have small classes/headers and keep hierarchies (inclusion depths) shallow.

Headers requiring specific inclusion order

Another (arguably opposite) situation is also possible for a header: it does not include all its dependencies.

Let’s say that header B.h declares something that depends on types found in A.h but B.h does not have #include "A.h" in it.

Given that the headers are rarely compiled on their own (i.e., they are not regularly checked for dependencies in isolation as part of compilation process), this issue will not be noticed until B.h is included into a compilation unit (C.c). Then compilation of C.c fails somewhere inside B.h because some of the required types from A.h are not defined at the first point of use.

A quick fix would be to include these two files A.h and B.h in that specific order inside C.c, which will make the latter source file buildable again.

It can be said that B.h has a defect of not being self-sufficient. Whenever it has to be included, it must be preceded by the inclusion of A.h. This dependency is not explicit and will be a repeating unpleasant surprise for a user of B.h. Over time this tends to create maintenance problems when lists of included headers cannot be rearranged or cleaned up because nobody really knows what depends on what.

Enforcing every header in your project to be self-sufficient is a better discipline. In fact it is not that hard to follow by always putting any given header into the first inclusion position in at least one source file. By adhering to this rule, one can be certain that all #include lines can be organized in an arbitrary order and they all still have the same meaning that will not break compilation of any compilation unit (but see the discussion about header guards below for some nuances).

What about intentionally non-self-sufficient headers?

In rare cases you might want to have a non-self sufficient header on purpose.

To parametrize some of the included declarations with additional information found elsewhere can sometimes be desirable. Usually it is done by #define-ning a symbol before including a file. For example, POSIX feature test macros work like this.

Had the standard preprocessor offered syntactical support to directly express it inside the directive, e.g., to write something like this: #include "B.h(parameters)", then it would had been a more explicit way to express the intention. With the existing preprocessor syntax, such an intent has to be explained by other means.

The reader might still be left wondering whether any particular order of #includes and #defines is accidental or intentional.

Here I see a parallel with templates of C++. One does not fully define a template-class but leaves it pending until exact template parameters are provided. Individual instantiations of such class are then checked for correctness. Of course this mechanism of C++ is much more strict and robust, which helps with readability. The approach to parametrized headers is quite fragile and vague in comparison.

Include guards as a solution to multiple inheritance

Can the same header file be included more than once into the same compilation unit? The preprocessor does not prevent us from doing that. Oftentimes, the same header is included as a dependency for two adjacent and otherwise unrelated headers that happen to depend on the same type.

However this creates a problem with C++’s one definition rule. While functions and types can be (forward-)declared any number of times, they may only have one definition. Including the same header twice causes definitions within it to be repeated. Despite both definitions being identical, it is still usually enough to break the compilation or linking.

A typical solution for this problem is to make the second and subsequent inclusions of a given header to appear as empty text. Both header guards #ifndef HEADER_H ; #define HEADER_H ; #endif and non-standard but widely accepted#pragma once approaches achieve this effect.

There are at least two more solutions to this issue however.

  1. Indeed require including any header file at most once in any translation unit. In practice, this is incompatible with the notion of self-sufficient headers. All the dependencies of all header files must be listed before them and appear exactly once. For big projects, it would be quite a chore to maintain such strict rule in all its compiled files.
  2. To make headers only contain forward declarations and never contain definitions. This forces to design your software system around opaque types, layouts of which are not known to their users and instances of which can only be manipulated by functions declared in the same header. This is not a bad idea per se, as it ensures strong isolation around abstract data types. But it also quite awkward to achieve in C and C++ which oftentimes force you to “expose the guts” of classes (e.g. how C++ forces to announce private parts of classes in public definitions).

In my practice, the include guard approach is the most popular one.

There is a direct parallel with multiple inheritance problem. When a header is included into another file, it is essentially “inherited” from.

If class A inherits from two classes B and C which in turn inherit from the same class D, it creates an ambiguity for which “copies” of D we should be accessing in A. Different languages offer different solutions to it, from outright prohibiting multiple inheritance to providing syntax to control and spell out rules to resolve possible ambiguity.

The approach with include guards is a solution that coalesces multiply-inherited pieces into one copy.

Imports versus includes

Let’s compare the mechanism of #include against more modern import found in Python and similarly added in C++20.

  • #include is text manipulation. Its primary modern usage is describing interfaces. It helps to save time on maintenance of external linkage declarations. But the text substitution mechanism can also be used for other various manipulations over source contents visible to compiler. Imports, on the other hand, are designed exclusively for the purpose of communicating interfaces of individual modules.
  • #include is all-or-nothing. Everything found in the header becomes visible when it is included. A lot of care should be made in order to not over-share unnecessary details or dependencies. Imports are designed to make only a subset of another file accessible, but respecting the public/private separation (which is language-specific if we are to looks at how Python/C++ do them but it is present in both cases).
  • #include is recursive in its nature. All declarations and definitions coming from “inner” #includes are preserved and pollute the namespace of the compiled source file. Imports are usually more constrained in what they make visible to the consumer of a module. Dependencies of a module do not become visible to its user at the moment of import.

Written by Grigory Rechistov in Uncategorized on 18.02.2024. Tags: oop, lsp, isp, solid,


Copyright © 2024 Grigory Rechistov