Conditional logic and cross-platform handling
suggest changeIn a nutshell, conditional pre-processing logic is about making code-logic available or unavailable for compilation using macro definitions.
Three prominent use-cases are:
- different app profiles (e.g. debug, release, testing, optimised) that can be candidates of the same app (e.g. with extra logging).
- cross-platform compiles - single code-base, multiple compilation platforms.
- utilising a common code-base for multiple application versions (e.g. Basic, Premium and Pro versions of a software) - with slightly different features.
Example a: A cross-platform approach for removing files (illustrative):
#ifdef _WIN32
#include <windows.h> // and other windows system files
#endif
#include <cstdio>
bool remove_file(const std::string &path)
{
#ifdef _WIN32
return DeleteFile(path.c_str());
#elif defined(_POSIX_VERSION) || defined(__unix__)
return (0 == remove(path.c_str()));
#elif defined(__APPLE__)
//TODO: check if NSAPI has a more specific function with permission dialog
return (0 == remove(path.c_str()));
#else
#error "This platform is not supported"
#endif
}
Macros like _WIN32
, __APPLE__
or __unix__
are normally predefined by corresponding implementations.
Example b: Enabling additional logging for a debug build:
void s_PrintAppStateOnUserPrompt()
{
std::cout << "--------BEGIN-DUMP---------------\n"
<< AppState::Instance()->Settings().ToString() << "\n"
#if ( 1 == TESTING_MODE ) //privacy: we want user details only when testing
<< ListToString(AppState::UndoStack()->GetActionNames())
<< AppState::Instance()->CrntDocument().Name()
<< AppState::Instance()->CrntDocument().SignatureSHA() << "\n"
#endif
<< "--------END-DUMP---------------\n"
}
Example c: Enable a premium feature in a separate product build (note: this is illustrative. it is often a better idea to allow a feature to be unlocked without the need to reinstall an application)
void MainWindow::OnProcessButtonClick()
{
#ifndef _PREMIUM
CreatePurchaseDialog("Buy App Premium", "This feature is available for our App Premium users. Click the Buy button to purchase the Premium version at our website");
return;
#endif
//...actual feature logic here
}
Some common tricks:
Defining symbols at invocation time:
The preprocessor can be called with predefined symbols (with optional initialisation). For example this command (gcc -E
runs only the preprocessor)
gcc -E -DOPTIMISE_FOR_OS_X -DTESTING_MODE=1 Sample.cpp
processes Sample.cpp in the same way as it would if #define OPTIMISE_FOR_OS_X
and #define TESTING_MODE 1
were added to the top of Sample.cpp.
Ensuring a macro is defined:
If a macro isn’t defined and its value is compared or checked, the preprocessor almost always silently assumes the value to be 0
. There are a few ways to work with this. One approach is to assume that the default settings are represented as 0, and any changes (e.g. to the app build profile) needs to be explicitly done (e.g. ENABLE_EXTRA_DEBUGGING=0 by default, set -DENABLE_EXTRA_DEBUGGING=1 to override). Another approach is make all definitions and defaults explicit. This can be achieved using a combination of #ifndef
and #error
directives:
#ifndef (ENABLE_EXTRA_DEBUGGING)
// please include DefaultDefines.h if not already included.
# error "ENABLE_EXTRA_DEBUGGING is not defined"
#else
# if ( 1 == ENABLE_EXTRA_DEBUGGING )
//code
# endif
#endif