The Database Managers, Inc.

Contact The Database Managers, Inc.


Use an RSS enabled news reader to read these articles.Use an RSS enabled news reader to read these articles.

How to resolve 'Cannot create pre-compiled header: initialized data in header' compile-time problems.

by Curtis Krauskopf

Pre-compiled headers are one of the most important compiler features for reducing compilation times. Pre-compiled headers allow the compiler to reduce the project's overall compile time because information is reused from previous compilations.

Know the Rules

Here are some rules to keep in mind when using precompiled headers:

  1. Use precompiled headers on compilation units that are relatively large, relatively complicated or have many external dependencies. The compiler's bookkeeping associated with precompiled headers takes some time too. For simple units, the bookkeeping overhead outweighs the benefits of precompiled headers. When used judiciously, the time saved by using a precompiled header will vastly outweigh the compiler's bookkeeping overhead.
  2. Precompille headers that are maturely developed and are not changed frequently. The bookkeeping overhead of frequently recreating a precompiled header might outweigh any gains from using the precompiled header.
  3. Limit the number of precompiled header units that are in a precompiled header. Although it might be tempting to create a precompiled header that includes all of C libraries, and all of the C++ libraries and all of the STL libraries and even the Boost libraries (just in case any of them are used), you'll discover that the disk I/O associated with the enormous size of the precompiled header is a boat anchor that drives down the precompiled header's benefits. Keep it simple. Limit the precompiled header to only the header units that you're actually using in the project.
  4. Properly guard every header so that it is only compiled one time. This means using a #ifdef and a unique #define in every header module. A template example of this for a file called MyModule.h is:
    #ifndef MyModule_H
    #define MyModule_H
    ...
    ... // insert the contents of the header file here
    ...
    #endif
  5. Limit the precompiled header units only to units that are frequently used across multiple compilation units.
  6. Do not put data in precompiled header units. Fortunately, the compiler will generate a warning message W8058 (a BCC32 warning). Here is an example:
    [BCC32 Warning] somefile.hpp(10): W8058 Cannot pre-compile
    header: initialized data in header
    
    This warning says that line 10 of somefile.hpp contains code that uses data.

The last rule is sometimes the most difficult one to obey but at least the compiler will warn us when we break the rule.

String Example

This article will use the simplest possible example: a main module that uses a single function in a static library. To make it clear which module is which, the main module will be called main.cpp and the library is called library.cpp (pretty original, huh?). The library has one header file called library.h. Both the library.cpp and the main.cpp include the library.h file.

Figure 1: A simple relationship between
main.cpp and a library it uses.

An example of data in a header file is in Listing 1.

Library.h:
#ifndef Library_H
#define Library_H

#include <string>

bool hasWhiteSpace(const std::string &str,
const std::string &whitespace = " \n\t");
#
endif
Listing 1: The default parameter in the function prototype contains data.

The hasWhiteSpace function in the library allows the programmer to pass a string to it and check if the string has any of the specified whitespace characters. For the programmer's convenience, the function has a default parameter that defines several common whitespace characters. However, the programmer can always choose their own set of whitespace characters and the hasWhiteSpace function will analyze the string with those specific characters.

The code in Listing 2 is an example of calling the hasWhiteSpace function from main.cpp:


std::string userInput = "only\ttab";
bool parseIt = hasWhiteSpace(userInput);
bool hasBlank = hasWhiteSpace(userInput, " ");

Listing 2: Example code that calls the
hasWhiteSpace library function.

In the example, the first call to hasWhiteSpace uses the default parameter to check if the value in the userInput string has a blank, a newline character or a tab character.

The next call to hasWhiteSpace in the example checks specifically if the userInput string has a blank character.

Fixing the W8058 warning

There are several techniques for fixing the W8058 warning:

  1. Overload the function
  2. Make the value a constant
  3. Use an empty string

Let's look at an example of each solution.

Overload the function

Overloading the function has the effect of removing the default parameter. One overloaded function is needed for each default parameter plus one overloaded function is needed for all of the parameters. For example, a function with 3 default parameters needs 4 overloaded functions (one for each default parameter plus the original function with all of the parameters). Table 1 shows an example of the overloaded functions. Caveat: the default parameters in Table 1 do not cause a W8058 warning. I'm showing a simple example of how a function with default parameters can be recoded to be overloaded and provide the same functionality.

Function x with default parameters:
int x(int a = 1, int b = 2, int c = 3);

// Function x overloaded:
int x();
int x(int a);
int x(int a, int b);
int x(int a, int b, int c);

Table 1: Function x() has three default parameters.
Four overloaded function definitions are needed to
reproduce all of the permutations available with the
single x() function that uses default parameters.

The example for hasWhiteSpace in Listing 1 has only one default parameter, so two overloaded functions are in Listing 3.

Library.h:
#ifndef Library_H
#define Library_H

#include <string>

bool
hasWhiteSpace(const std::string &str); bool hasWhiteSpace(const std::string &str, const std::string &whitespace); #endif
Listing 3: hasWhiteSpace is overloaded to remove the initialized data in the header.

The implementation for the first overloaded function (hasWhiteSpace(str)) is in Listing 4.

Library.cpp:

bool hasWhiteSpace(const std::string &str)
{
  return hasWhiteSpace(str, " \n\t");
}

Listing 4: The hasWhiteSpace function is overloaded
to put the initialized data in the .cpp module.

The trick for removing the initialized data from the header is to overload the function and put the initialized data in the .cpp file. The overloaded functions provide the same interface that the original function using default parameters provided. The interface in the .h file no longer has an initialized data dependency.

The down-side to this solution is that it makes maintenance a little more difficult because when the maintenance programmer (you?) needs to change the value of a default parameter in an overloaded function, you'll have to make the same change in all of the other overloaded functions. This is the kind of thing that can easily be overlooked and then you'll wonder why some function calls are using the new behavior and some function calls are using the old behavior.

If you don't mind the relatively minor runtime overhead, you could chain the implementations so that they progressively call the next overloaded function. For example, going back to the four overloaded functions in Table 1, the implementation would look like Listing 5.


int x() {
    return x(1);
}

int x(int a) {
    return x(a, 2);
}

int x(int a, int b) {
    return x(a, b, 3);
}

int x(int a, int b, int c) {
    // original implementation goes here
}

Listing 5: The implementation of the four
overloaded functions from Table 1 chains the
calls to the x(int, int, int) function. This means
that the default parameters are only used in one
place in the implementation's .cpp file.

Another down-side to the "overloading the functions" solution is that it creates a plethora of overloaded functions. Although I personally don't have any problem with lots of overloaded functions, I've had colleagues tell me that it makes the interface more difficult to understand. Because I respect their opinions, I try to limit the number of overloaded functions I have in the libraries I write.

Make the value a constant

Another way to bypass the W8058 warning is to make the default parameter an externally defined constant.

Listing 6 shows an example of the hasWhiteSpace function with an externally defined constant string. Listing 7 shows the implementation of the hasWhiteSpace function in the Library.cpp file. The Const_DefaultWhitespace symbol is defined in the .cpp file.

Library.h:
#ifndef Library_H
#define Library_H

#include <string>

extern std::string Const_DefaultWhitespace;
bool hasWhiteSpace(const std::string &str, 
const
std::string &whitespace = Const_Default_Whitespace); #endif
Listing 6: Use an externally defined constant for the default parameter.

Library.cpp:

#include "Library.h"

const std::string Const_DefaultWhitespace = " \n\t";

bool hasWhiteSpace(const std::string &str, 
                   const std::string &whitespace /* = " \n\t" */ )
{
 	// function implementation goes here
}

Listing 7: The Const_DefaultWhitespace symbol is defined in the Library.cpp file. One of the coding conventions I use with default parameters is to comment the parameter's default value in the implementation. This reminds me, while I'm writing or maintaining the function, which parameters have default values and what the default values are.

To me, this seems to be an improvement over the multiple-overload solution because the default parameters are still defined in one place and because there isn't an explosion of overloaded functions cluttering the .h file.

Use an Empty String

This solution has three prerequisites:

  1. The compiler is a Borland compiler
  2. Borland's VCL can be compiled into the main module
  3. An empty string in the default parameter would not normally be a valid value for the function.

Default empty strings do not exist in the STL. For Borland compilers, however, the VCL offers three types of empty string constants:

  • System::UnicodeString EmptyStr;
  • System::WideString EmptyWideStr;
  • System::AnsiString EmptyAnsiStr;

All three of these strings are defined in the SysUtils.hpp header (which is automatically included when #including <vcl.h>).

By refactoring the second parameter of hasWhiteSpace from a std::string to an AnsiString, an EmptyAnsiStr can be used as a default parameter. This works because EmptyAnsiStr is defined as an extern in SysUtils.hpp. It also works because an empty string would not normally be a valid value in the second parameter of the hasWhiteSpace function.

Putting this all together, Listing 8 has the solution for the header file and Listing 9 has the solution for the .cpp file.

Library.h:
#ifndef Library_H
#define Library_H

#include <string>
#include <VCL.h>

bool hasWhiteSpace(const std::string &str, 
                   const std::string &whitespace);
bool hasWhiteSpace(const std::string &str, 
                   const AnsiString &whitespace = EmptyAnsiStr);

#endif
Listing 8: This solution overloads the hasWhiteSpace function with a default parameter this is defined in SysUtils.hpp.

Library.cpp:
#include "Library.h"
bool hasWhiteSpace(const std::string &str,
const std::string &whitespace) { ... ... // The original hasWhiteSpace implementation goes here ... } bool hasWhiteSpace(const std::string &str, const AnsiString &whitespace) { if (whitespace == EmptyAnsiStr) { return hasWhiteSpace(str, std::string(" \t\n")); } else return hasWhiteSpace(str, std::string(whitespace.c_str())); }
Listing 9: The hasWhiteSpace function that is overloaded to accept an AnsiString uses the EmptyAnsiStr empty string defined in SysUtils.hpp.

The Library.cpp contains two implementations of hasWhiteSpace. One takes an an AnsiString as the second parameter and the other takes a std::string as the second parameter.

The careful reader will want to cry "Shenanigans" because this solution is really no different than the first solution -- both overload the function.

While that is true, the overloading would not have been needed if the original string had been one of the VCL variants: UnicodeString, WideString or AnsiString. In that case, the Library.h file would only have been changed to replace the default parameter with the appropriate SysUtils.hpp-defined symbol: either EmptyStr, EmptyWideStr or EmptyAnsiStr. The hasWhiteSpace function in Library.cpp file would still have been modified to detect if an empty string had been passed to it.

Unfortunately, this solution only moves the problem to another module because CB2010 complains that Dialogs.hpp also has initialized data in the header:

[BCC32 Warning] Dialogs.hpp(1296): W8058 Cannot create pre-compiled header: initialized data in header

This is a problem in the Dialogs.hpp file that Embarcadero has known about for a long time but for some reason has not been fixed yet.

Int Example

Sometimes data in a header looks like this:


static unsigned sym = 127;

The solution for this problem is to add 'const' to the declaration. const can be on either side of the static keyword:


static const unsigned sym = 127; // okay
or
const static unsigned sym = 127; // okay 

Conclusion

Precompiled headers reduce the compile-time by allowing the compiler to cache its work for future compilations.

One of the main restrictions for using precompiled headers is that data can not be initialized in any precompiled header.

Fortunately, there are four different techniques for solving this problem: overloading the function, making the default parameter a constant, using an empty string constant or making the variable declaration const.

Copyright 2003-2010 The Database Managers, Inc.

Popular C++ topics at The Database Managers:

Services | Programming | Contact Us | Recent Updates
Send feedback to: