That's not really what the pre-processor is for. At least not if you're talking about how it's typically used with header files.
In C++ each compilation unit (.cpp file, and recursively all the header files it includes), are compiled into an object file. Once all the source files are compiled into object files, they are linked into an executable file (or a DLL, or a static library, depending on project type).
During the link step, the order in which the object files are linked together is not specified. It might be the order the object files were specified on the command line to the compiler. Depending on the build system, that order might simply match the order of the directory listing returned by the filesystem, or it might be the order source code was added to the project file, or it might just be alphabetical order. At any rate, the order is not specified by the C++ standard.
The order of linking of the object files will affect the initialization order of global variables in those object files. If file A is linked before file B, you might expect the globals in file A to be initialized before the globals in file B. If you reverse the ordering of the linking, you might expect to reverse the order in which global variables are initialized.
This could be a problem if the initialization of a global in file A depends on the value of a global in file B.
// A.cpp
ObjectTypeA objectA(objectB.getValue());
// B.cpp
ObjectTypeB objectB;
If
objectA is initialized first, then its constructor call will try to use the not yet initialized
objectB in the expressions
objectB.getValue(). That will likely result in accessing uninitialized memory, which is quite likely to result in a crash.
There is a workaround, though it can seem a bit hacky at times. You put all global variables in the same .cpp file. The order of initialization within a compilation unit is well defined. The variables are initialized in the order they are defined. That means you could write the following:
// Globals.cpp
ObjectTypeB objectB;
ObjectTypeA objectA(objectB.getValue());
So could you pre-process stuff and fix the problem? I suppose you could for this particular case. You could do something really ugly and non-standard, like include .cpp files instead of .h files. That could combine the whole project into one compilation unit, wherein initialization order is well defined, based on definition order. You'd then need to carefully order your
#include lines:
// CombinedFiles.cpp
// Include .cpp files (not .h files!)
#include "B.cpp"
#include "A.cpp" // A depends on B, so must come after it
Of course then you lose the firewall aspect of having multiple compilation units. If you change anything, even a comment, you now have to recompile the entire project since it's all one compilation unit. Considering how slow C++ compilers typically are, even for medium sized projects, that's a major pain. For a large project, a full recompile could take a few hours. If you keep code separated into different source files, you only need to recompile a section when something in that compilation unit changes. The linking step at the end is pretty fast.
Another problem where globals can get you in trouble with initialization, and where re-ordering things won't help you, is when a global ends up making a call to itself from its constructor. That can happen in all sorts of strange and convoluted ways, and if often quite indirect.
Case in point, I just noticed something today in op2ext. We have a global logging object. The logging object logs messages to a file. When it is constructed, it tries to open the log file for output, which could fail. If it fails, it reports the error to the user with a message box. Over time, use of the message box grew, so now it is used to report errors in several places in the code. At some point, probably weeks later, it was decided that these errors shouldn't just be displayed, but also logged.
So now, if the logger fails to open it's log file, it reports the error, which tries to log the error.
Amazingly it doesn't crash, but that's a bit to do with luck. The logging object is initialized just enough before reporting the error that the reporting system can use it without crashing, though it can't write anything to the non-opened file. I'm pretty sure it's still undefined behaviour though.
The structure looks something like this:
// Logger.cpp
Logger::Logger() : logFile(logFileName) {
if (!logFile.open()) {
PostErrorMessage("Failed to open log file");
}
}
// LotsOtherFiles.cpp
// ...
// PostErrorMessage("Blah!");
// ...
// PostErrorMessage("Blah!");
// ...
// PostErrorMessage("Blah!");
// PostErrorMessage.cpp
PostErrorMessage(std::string message) {
auto formattedMessage = getTimeString() + getExtraDetail() + message
logger.Log(formattedMessage); // Added a few weeks later after PostErrorMessage became a general reporting tool
MessageBox(formattedMessage);
}
Edit: I would just like to add that I was fully responsible for adding the
logger.Log(formattedMessage); line. It seemed like a good idea at the time.