10.2.4. #pragma and #ifndef: Actor 2

Time: 00:06:25 | Download: Large, Large (CC), Small | Streaming (CC) | Slides (PDF)
Review

Implementing the previous version of the Actor example in a single file allowed us to focus on the mechanics of inheritance, but putting all the classes in a single file in an object-oriented program isn't common or realistic. The second version still uses the over-simplified Person, Actor, and Star classes but adopts the more common and realistic practice of putting each class specification in a separate header file. This approach increases the potential for reusing classes, but it also increases the potential for unexpected errors.

The classes remain unchanged from the first version, so this section focuses on the syntax necessary to manage a multi-class program. If a class specification only contains member variables and function prototypes (both members and non-members), then the specification is purely a declaration. Multiple declarations don't interfere with compilation if each declaration is the same. But if the class includes any function bodies, then those bodies form a function definition. Whereas multiple declarations don't cause a problem, multiple definitions do.

Prototypes Only Inline Functions
class Person
{
    private:
        string    name;

    public:
        Person(string n) : name(n);

        void display();
};
class Person
{
    private:
        string    name;

    public:
        Person(string n) : name(n) {} 

        void display() { cout << name << endl; } 
};
(a)(b)
Alternate class specifications.
  1. The first class specification only contains declarations or prototypes and may be included in a program repeatedly without error. Following this organization, programmers define the constructor and display function in a source code file.
  2. The second class specification contains function bodies (highlighted), which makes the functions definitions. Including this class specification in a program more than once causes a "multiple definition" error when we compile the program.

So far, I haven't suggested how a class specification might become included in a program more than once. The Actor2 header files and the main function demonstrate a common way that this can occur. C++ provides two preprocessor mechanisms (the ANSI standard incorporates only one) to prevent this error by ensuring that a class specification is included in each source code file exactly one time.

#include Guard

The classic solution to the multiple file inclusion problem is called an include guard or macro guard. The include guard is implemented by the preprocessor using three directives:

#ifndef
The opening preprocessor directive is read as "if not defined." A symbolic constant name follows the directive. If the name is not defined, the following code is read by the preprocessor and included in the program. Alternatively, if the name is defined, the preprocessor skips all code until it reaches the matching #endif directive. The preprocessor doesn't include the skipped code in the program.
#define
Defines or creates the symbolic constant. If the constant name is already defined, the #ifndef directive skips the definition. The more general usage requires a value to associate with the constant name. As used in the guard, the definition is significant, but the value is not, so the preprocessor automatically sets the value to 1.
#endif
The "end if" directive ends or closes the #ifndef directive. The code appearing between #ifndef and #endif is conditionally included in or excluded from the source code file.
The #include guard directives. The constant name following the #ifndef and #define directives must be unique throughout the program (i.e., used in only one header file). Traditionally, programmers create unique constant names from the header file name (which programmers derive from the class name) by following two or three steps:
  1. Convert all file-name characters to upper case (a traditional convention for symbolic-constant names)
  2. Prepend an underscore character at the beginning of the name, and replace the "." with an underscore
  3. Some programmers optionally add an underscore at the end of the name
The #include, or macro guard, is a little "bulky" and complicated, but ANSI C and C++ standards include all three directives. So, any ANSI-compliant compiler can correctly process an #include guard.

#pragma once

The #pragma preprocessor directive provides additional information to the compiler system. It specifies a non-standard behavior that pragmatically solves some problem using the unique features of a vendor's hardware or software. As such, "pragmas" are not covered by the ANSI C or C++ standards and may not be portable (i.e., be easily moved from one computer to another). Like the #include guard, the #pragma once directive ensures that a block of code is included in a source code file exactly once. It is more compact (i.e., has much less code) than a guard and avoids the problem of creating unique identifiers or constant names. Although most contemporary compilers on Windows, macOS, and Linux support #pragma once, doing so is not yet required by the ANSI standard, so it is not guaranteed to work with all compilers.

Robust Header Files

C++ classes are typically implemented as two files: a header file and a source code file. The header file contains the class specification, prototypes for large functions, and complete definitions for small functions. The source code file contains static member variables and the definitions for large functions. This organization simplifies writing programs based on a client and supplier model. The application program is the client, while the class, represented by the header and the source files, is the supplier. The client or application program only needs to #include the supplier's header file to use its services. The compiler system compiles and links the supplier's source code file with the application.

Moving the Actor classes to separate header files has a significant and easily overlooked consequence. The three classes and the main function of the example use the string and iostream library classes. One set of include directives provides the necessary class specifications to all parts of the single-file version. But simply copying the classes to new, separate header files can inadvertently create error-prone code.

#include <iostream>
#include <string>
using namespace std;
#include "Person.h"
#include "Actor.h"
#include "Star.h"
#include "Person.h"
#include "Actor.h"
#include "Star.h"
#include <iostream>
#include <string>
using namespace std;
(a)(b)
Robust vs. error-prone header files. The code blocks represent two typical ways programmers can include the header files needed by a main function using the three Actor classes. The blocks contain the same code but in a different order. The using statement must follow the inclusion of the system headers (those whose names are in angle brackets), but its placement is otherwise insignificant. For this example, imagine that we have moved each class specification to a separate header file but without the highlighted code.
  1. This sequence compiles without error because the highlighted code makes the string and iostream class specifications available to the following Actor classes.
  2. This sequence will not compile. The preprocessor reads the code from the top downwards, so the string and iostream classes are unspecified, and the class names are undeclared when the preprocessor processes the Actor classes.
Adding the highlighted code to each Actor class makes the classes more robust and easier for application programmers to use. Robust code is "capable of performing without failure under a wide range of conditions." The robust version of the Actor example is insensitive to the header file inclusion order in the client code.
Our operational rule of thumb - almost our mantra - is, "Whenever we use a class in any file, we #include its header in that file."

Actor2 Header Files

#ifndef _PERSON_H_
#define _PERSON_H_

#include <iostream>
#include <string>
using namespace std;


class Person
{
	. . .
};

#endif
#pragma once

#include <iostream>
#include <string>
using namespace std;


class Person
{
	. . .
};
 
 
 
Person.h. Two alternate ways of ensuring that a class specification is included in a source code file exactly once. The classic guard requires using the same constant name with both the #ifndef and #define directives. The Person class has a member variable and a constructor parameter that are instances of the string class, so the string header file is #included in the Person header file.
#ifndef _ACTOR_H_
#define _ACTOR_H_

#include <iostream>
#include <string>
#include "Person.h"
using namespace std;


class Actor : public Person
{
	. . .
};

#endif
#pragma once

#include <iostream>
#include <string>
#include "Person.h"
using namespace std;


class Actor : public Person
{
	. . .
};
 
 
 
Actor.h. The #include guard works here as described in the previous figure. The inheritance relationship, including the Actor constructor, is based on the Person class. That means that the Person class specification precedes the Actor class. Inheritance is a one-way relationship: an Actor "knows about" a Person, but an Actor doesn't "know about" a Star, so the Star header is not needed.

Like the Person class, Actor has a member variable and a constructor parameter that are instances of the string class, so the string header is #included in the Actor header file. Technically, doing this is unnecessary: the Actor header file includes the Person header, which includes the string header file. However, imagine an application program that uses the Actor class but does not directly deal with a Person. Following the best object-oriented programming practice, the Actor class designer should not require application programmers to "know" the implementation details of the Actor class, including that it subclasses Person. (For example, how much do you know about how C++ implements its string class?)

#ifndef _STAR_H_
#define _STAR_H_

#include <iostream>
#include <string>
#include "Actor.h"
using namespace std;


class Star : public Actor
{
	. . .
};

#endif
#pragma once

#include <iostream>
#include <string>
#include "Actor.h"
using namespace std;


class Star : public Actor
{
	. . .
};
 
 
 
Star.h. Using the inheritance relationship and the Star and Actor classes, the figure illustrates the similarities between the #include guard and the #pragma once directive. It also demonstrates the appropriate header files to #include in Star.h: string, because the constructor uses it, and Actor to support inheritance. However, Star does not directly depend on Person - it doesn't appear anywhere in the Star class - so the Star header doesn't include the Person header file. Both the #include guard and the pragma directive guarantee that the Actor header file is included exactly once.

Each Actor2 header file includes the C++ string header. The multiple inclusions don't cause errors because the string class employs a guarding mechanism, either an #include guard or a #pragma once. We unknowingly relied on this standard practice when we created our first multi-file program (Time, see figures 2 and 3) and placed the #include <iostream> directive at the beginning of each source code file. Thus is the value of the guarding mechanism: programmers can use existing classes and functions with little regard for their implementation details.

Actor2 Source Code File

#include "Person.h"
#include "Actor.h"
#include "Star.h"


int main()
{
	Person	director("Steven Spielberg");
	director.display();

	Actor	sidekick("Harvey Korman", "Dilbert");
	sidekick.display();

	Star	big_star("John Wayne", "Cranston Snort", 5000000);
	big_star.display();

	return 0;
}
driver.cpp. The main function, the sole content of driver.cpp, represents an application program that uses the classes specified above. It uses the class names as type specifiers to define the variables director, sidekick, and big_star. It also uses the class names to call the class's constructor functions.

The program demonstrates our operational rule of thumb or mantra: whenever we use a class name, we include the corresponding header file - even if some of the inclusions are superfluous. Including Person.h and Actor.h is superfluous because Star.h incorporates all three class specifications into the program. Nevertheless, making our classes sufficiently robust so that multiple inclusions don't cause compiler errors makes them easier for application programmers. Following this practice, it's also easy to see how a program might include a class specification more than once.

Header Files Summary

Rules of thumb for using header files:

  1. Each class should have its own header (i.e., .h) file
  2. If a class has any "long" functions (generally more than three or four statements), it should also have a source code (i.e., .cpp) file
  3. Programmers should define long functions and static member variables in the source code file
  4. Prevent "multiple definition" errors stemming from inlined functions by guarding class specifications:
    1. Use an include guard: #ifndef / #define / #endif
    2. Or use #pragma once
  5. Be economical with #include directives: when you use a class, include its header, but don't include extraneous header files
  6. The compiler system reads and processes each file once, from top to bottom, and every identifier or name encountered by the compiler must be declared before it can be used