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.
Hey, I was going to take credit for that line of code.
Ok, fine, you can have it! :P
...
Actually, if we want to play the blame game... ;)
git blame <previous commit SHA> -- srcStatic/GlobalDefines.cpp
... somewhere in PostErrorMessage
...
f52c2711 GlobalDefines.cpp (Brett208 2018-12-21 15:34:42 -0500 29) logger.Log(formattedMessage);
...
It was you that wrote it! ;D
Don't you just love version history. It never lies. ;)
... Though I'm pretty sure I was the one that suggested that line.
I normally try to avoid globals. Though with something like logging, you really don't want to be passing a logging object around to *every* function in your app that might want to log some event or problem.
In the end, we changed the logging to use global functions, and added protective layers around the underlying objects so they aren't really global anymore. At least their visibility isn't global. They may still be statically initialized if so desired, though it would be awkward to activate a logger at that point.
Global functions seem much less offensive than global objects.
I love global variables personally, but it makes saving them to file a real chore.
Global variables are generally evil (https://www.learncpp.com/cpp-tutorial/4-2a-why-global-variables-are-evil/)... though it really depends on how you use them, when and why. The very abridged version of Global Variables are EvilTM is that they can be changed anywhere at any time by any part of the program and it's difficult to know when they're changed. For smaller programs this isn't really a problem but when you get into programs that are tens of thousands (or even millions) of lines of code spanning dozens of source files, it becomes painful. That's where the get/set type functions come into place because there's only one way to get to and set the values of them.
If I need a 'global' value, coding best practices usually indicate use of get/set type functions. In my code they take the form of this:
/* Declared static so it can only be directly accessed within this module */
static int SomeValueIllNeedEverwhere = 0;
int SomeValueIllNeedEverywhere() { return SomeValueIllNeedEverywhere; }
void SomeValueIllNeedEverywhere(int i) { SomeValueIllNeedEverywhere = i; }
Granted this is useful in C and C++ -- C# has the concept of Properties which is an amazing thing that I truly love about its design (e.g., public int SomeValue { get; set; }, though everything does have to be wrapped in a class like in Java).
Keep in mind, of course, that the above code sample is trivial. But say, for instance, that later on you want value validation when you set the variable. Instead of having to modify every piece of code that touches the variable, you can just put the validation code in the setter function and you're done.
All of this has a term -- encapsulation. It's the cornerstone of defensive programming and a good concept to learn and use liberally.