Modules are the way of the future, but until the future arrives, we are stuck with #include files. Billions of lines of C++ code currently use #include files, so you need to know how they work. With a little bit of discipline, you can still separate interface from implementation and achieve much of what modules offer.
Interfaces As Headers
The basic principle is that you can define any function or global object in any source file. The compiler does not care which file contains what. As long as it has a declaration for every name it needs, it can compile a source file to an object file. (In this unfortunate case of convergent terminology, object files are unrelated to objects in a C++ program.) To create the final program, you have to link all the object files together. The linker doesn’t care which file contains which definition; it simply has to find a definition for every name reference that the compiler generates.
The Interface Header for rational in rational.hpp
The rational Implementation in rational.cpp
The main Function Using the rational Class in the main.cpp File
Now compile main.cpp and rational.cpp, then link them together to produce a working C++ program. An IDE takes care of the details for you, provided both source files are part of the same project. If you are using command-line tools, you can invoke the same compiler, but instead of listing source file names on the command line, list only the object file names. Alternatively, you can compile and link at the same time, by listing all the source file names in one compilation.
That’s the basic idea, but the details, of course, are a little trickier. For the remainder of this Exploration, we’ll take a closer look at those details.
Inline or Not Inline
Because Listing 42-1 is not a module, functions defined inside the class declaration are implicitly inline. But I declared them with the inline keyword anyway. This is a good practice to remind the human reader that these functions are inline. It also promotes a hybrid style of programming.
Creating a Module from a Header
Once again, working with modules makes coding easy. The export keyword can apply to all declarations in a brace-enclosed block. In this case, the #included header contains a single class declaration, but it can include much, much more.
Quotes and Brackets
All of the standard library’s #include directives used angle brackets, such as <iostream>, but #include "rational.hpp" uses double quotes. The difference is that you should use angle brackets only for the standard library and system headers, although some third-party libraries recommend the use of angle brackets too. Use double quotes for everything else. The C++ standard is deliberately vague and recommends that angle brackets be used for headers that are provided with the system and quotes be used for other headers. Vendors of add-on libraries have all taken different approaches concerning naming their library files and whether they require angle brackets or double quotes.
For your own files, the important aspect is that the compiler must be able to find all your #include files. The easiest way to do that is to keep them in the same directory or folder as your source files. As your projects become larger and more complex, you probably will want to move all the #include files to a separate area. In this case, you have to consult your compiler documentation, to learn how to inform the compiler about that separate area. Users of g++ and other UNIX and UNIX-like command-line tools typically use the -I (capital letter I) option. Microsoft’s command-line compiler uses /I. IDEs have a project option with which you can add a directory or folder to the list of places to search for #include files.
For many compilers, the only difference between angle brackets and quotes is where it looks for the file. A few compilers have additional differences that are specific to that compiler.
In a source file, I like to list all the standard headers together, in alphabetical order, and list them first, followed by the #include files that are specific to the program (also in alphabetical order). This organization makes it easy for me to determine whether a source file #includes a particular header and helps me add or remove #include directives as needed.
Include Guards
One very important difference between modules and #include files is that a module can be imported multiple times with no ill effect. But #include-ing the same file more than once might repeat all the declarations in that file, which is not allowed. Listing 43-1 protects against this possible mistake with #ifndef RATIONAL_HPP_. The directive #ifndef is short for “if not defined,” so the first line tests whether RATIONAL_HPP_ is not defined, which is isn’t. The second line goes about defining it. An #endif closes the conditional at the end of the file. If the same file is #included again, RATIONAL_HPP_ is now defined, so the #ifndef is false, and the entire file is skipped, down to #endif. This include guard, as it is known, is a common idiom in header files. It is not needed in a module, but is harmless. (And remember from Exploration 34 not to begin the guard name with an underscore. I use a trailing underscore to ensure the name does not conflict with any real name that the header might declare.)
Forward Declarations
The <istream> header contains the full declaration of std::istream and other, related declarations, and <ostream> declares std::ostream. These are large classes in large headers. Sometimes, you don’t need the full class declarations. For example, declaring an input or output function in an interface requires informing the compiler that std::istream and std::ostream are classes, but the compiler needs to know the full class definitions only in the implementation file.
The header <iosfwd> is a small header that declares the names std::istream, std::ostream, and so on, without providing the complete class declarations. Thus, you can reduce compilation time for any file that includes a header by changing <istream> and <ostream> to <iosfwd>.
This is known as a forward declaration. You can use a forward declaration when the compiler has to know a name is a class but doesn’t have to know the size of the class or any of the class’s members. A common case is using a class solely as a reference function parameter.
If your header uses <iosfwd> or other forward declarations, be sure to include the full class declarations (e.g., <iostream>) in the .cpp source file.
extern Variables
Global variables are usually a bad idea, but global constants can be extremely helpful. If you define a constexpr constant, you can put that in a header and not worry about it again. But not all constant objects can be constexpr. If you need to define a global constant and cannot make it constexpr, you need to declare it in a header and define it in a separate source file, which you link with the rest of your program. Use the extern keyword to declare the constant in the header. Another reason to separate the declaration and definition of a global constant is when you may need to change the value of the constant but do not want to recompile the entire program.
Simple Header for Global Constants
Definitions of Global Constants
A Trivial Demonstration of globals.hpp
One-Definition Rule
The compiler enforces the rule that permits one definition of a class, function, or object per source file. Another rule is that you can have only one definition of a function or global object in the entire program. You can define a class in multiple source files, provided the definition is the same in all source files.
Inline functions follow different rules than ordinary functions. You can define an inline function in multiple source files. Each source file must have no more than one definition of the inline function, and every definition in the program must be the same.
These rules are collectively known as the One-Definition Rule (ODR).
The compiler enforces the ODR within a single source file. However, the standard does not require a compiler or linker to detect any ODR violations that span multiple source files. If you make such a mistake, the problem is all yours to find and fix.
The Original point.hpp File
The problem is that the order of the data member declarations is different from the order of the data members in the constructors’ initializer lists. It’s a minor error, but one that can lead to confusion, or worse, in more complicated classes. It’s a good idea to ensure the orders are the same. Suppose you decide to fix the problem by reordering the data members.
Then you recompile the program, but the program fails in mysterious ways. Some of your regression tests pass and some fail, including trivial tests that have never failed in the past.
_____________________________________________________________
_____________________________________________________________
With such limited information, you can’t determine for certain what went wrong, but the most likely scenario is that the recompilation failed to capture all the source files. Some part of the program (not necessarily the part that is failing) is still using the old definition of the point class, and other parts of the program use the new definition. The program fails to adhere to the ODR, resulting in undefined behavior. Specifically, when the program passes a point object from one part of the program to another, one part of the program stores a value in x_, and another part reads the same data member as y_.
This is only one small example of how ODR violations can be both subtle and terrible at the same time. By ensuring that all class definitions are in their respective header files, and that any time you modify a header file you recompile all dependent source files, you can avoid most accidental ODR violations.
Modules do not make ODR problems vanish, but they greatly reduce the likelihood that you will run into one. Because modules are distinct entities that the compiler knows about and have semantics of their own, the compiler can do more error-checking than it can with #include headers, which are just files and carry no additional semantic information. So when using modules, the compiler might be able to tell you that the implementation was compiled with a different version of the interface or that the interface for a particular class changed since the last time you compiled the implementation.
If you can write your own modules, I recommend that you do so, even if your tools do not fully support standard library modules yet. Judging from prereleases, it looks like importing standard library modules, such as import <iostream>, may be the last aspect of modules to be implemented. Just don’t let that stop you from writing your own.
Now that you have the tools needed to start writing some serious programs, it’s time to embark on some more advanced techniques. The next Exploration introduces function objects—a powerful technique for using the standard algorithms.