Visual Studio preprocessor macros in detail

The default Release and Debug version preprocessor macros in Visual Studio C/C++ projects can include some common macro definitions, some of which may depend on the project’s settings and target platform, in addition to which custom ones can be created as needed. Version:

Figure-1 Debug version preprocessing macro definition of a VS C/C++ project

WIN32:This is a standard macro for the Windows platform and indicates that the code is being compiled on a Windows operating system. If the WIN32 macro is not defined, conditional compilation directives in your code (such as #ifdef WIN32 or #ifndef WIN32) will not work as expected Work. However, most C++ programs that actually support cross-platform compilation use the _Win32 macro, as shown in the following code:

#ifdef _WIN32
                std::invoke(func, lockedCallback, args...);
#else
                std::__invoke(func, lockedCallback, args...);
#endif

The two macros work similarly, but typically _WIN32 is more common because it is the convention used by the Microsoft compiler and many Windows-related header files. The _WIN32 macro is usually predefined by the compiler, especially in Windows environments. The compiler automatically defines this macro for you when compiling a Windows application. This means you don’t need to define _WIN32 manually, it will appear automatically based on the target platform you choose.

Normally, when you use a Microsoft Visual C++ compiler (such as Visual Studio) to build a Windows application, the _WIN32 macro is automatically defined because this compiler is primarily used for development on the Windows platform. The _WIN32 macro is mainly used to distinguish code for Windows and non-Windows platforms. x64 applications compiled on Windows can still use the _WIN32 macro to distinguish the Windows platform from other platforms. So perhaps for historical reasons, _WIN32 is also defined on 64-bit platforms, but is usually used to represent Windows environments. _WIN64 This is a Windows x64 platform macro used to indicate that the target platform is 64-bit.

_DEBUG is usually used to indicate whether the code is compiled in debug mode. Code compiled in debug mode will enable debugging information, assertion checking, and other functions to allow developers to locate and fix problems while debugging. In release builds, it is common to define NDEBUG and undefine _DEBUG, thereby disabling debugging related features to increase execution speed and reduce generation The size of the binary file. The following are some typical application scenarios of the _DEBUG macro.

//Conditional assertions: In debug versions, assertions are usually used to check whether conditions in the code are true. This helps catch errors at runtime.
#ifdef _DEBUG
assert(x > 0);
#endif

//Debug log output:
#ifdef _DEBUG
DebugLog("Debug information");
#endif

//Insert test code
#ifdef _DEBUG
RunDebugTests();
#endif


//Memory leak detection
#define _CRTDBG_MAP_ALLOC // Include this macro to enable memory leak detection
#include <stdlib.h>
#include <crtdbg.h>

int main() {
    // In debug builds, enable memory leak detection
    #ifdef _DEBUG
    _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
    #endif

    //Allocate a piece of memory on the heap
    int* myArray = new int[100];

    // Forgot to release memory
    // In debug builds, the CRT will report a memory leak

    // In release builds, memory leak detection will be disabled
    // The program can execute faster after freeing the memory

    return 0;
}

_BIND_TO_CURRENT_VCLIBS_VERSION: This macro is related to the Visual C++ runtime library and is usually automatically added to the project to ensure compatibility with the current version of the VC++ runtime library. Link.

UNICODE: This macro indicates that the project is using the Unicode character set instead of the ANSI character set. It is typically used with the _TCHAR or wchar_t character types to ensure that the code can handle Unicode characters. The [L] prefix that we commonly see in C++ project code is used to define wide character strings. For example, L"Hello" defines a string encoded in wide characters. If UNICODE is not defined, the L prefix will have no effect. In addition, the common [_T] is also a macro, which switches the wide character or multi-byte character version of a string constant according to the situation defined by UNICODE. If UNICODE is defined, _T("Hello") will expand to L"Hello"; if UNICODE is not Definition, it will expand to "Hello".

Therefore, undefined UNICODE causes the wide character constant prefix L and macro _T to have no effect and the string literal will use the multibyte character encoding. If you need to support wide character sets and UNICODE, you usually need to define UNICODE in the project’s compilation settings so that _T and L can be used normally. prefix to write cross-platform and internationalized code.

STRICT: Enables strict type checking. This is typically used to ensure code compatibility with Windows API data types and prevent some potential type mismatch issues. If the STRICT macro is not defined, the Windows API may be more lenient in parameter types, allowing some data types to be passed that do not match well. This can reduce compiler strictness in some cases, but can also cause code to compile and run with type mismatches, increasing the risk of potential errors.

SECURE_SCL: The Secure Standard C++ Library (SCL) is an enhanced C++ standard library designed to improve the security of your code. In a Debug configuration, _SECURE_SCL is usually enabled for additional security checks. In Release configurations, _SECURE_SCL is usually undefined to improve performance.

HAS_ITERATOR_DEBUGGING: This macro is related to iterator debugging of the C++ standard library. In Debug configurations, iterator debugging is typically enabled, while in Release configurations, _HAS_ITERATOR_DEBUGGING is typically undefined to improve performance.

ITERATOR_DEBUG_LEVEL: This macro sets the debug level of the iterator. In the Debug configuration, this is typically set to 2 or higher for detailed iterator debugging. In a Release configuration, this is usually set to 0 to improve performance.

NO_DEBUG_HEAP: In a Debug configuration, _NO_DEBUG_HEAP is usually not defined in order to use the debug heap. In Release configurations, _NO_DEBUG_HEAP is typically defined to improve performance.

DELAYIMP_INSECURE_WRITABLE_HOOKS: This macro is used to control the behavior of delayed loading of linked libraries. It can be used to prevent certain types of attacks and is usually enabled in more advanced project settings.

Figure-2 How to add a custom build configuration item

As shown in [Figure-2] in addition to the default build configurations, you can also add custom build configurations as needed. To add a custom build configuration, you can follow these steps (using Visual Studio 2019 as an example):

  1. Open a project or solution.

  2. Go to the Solution Explorer window.

  3. In Solution Explorer, right-click your project and select Properties (or right-click the solution and select Properties to apply to the entire solution).

  4. In the properties page that pops up, expand the “Configuration Properties” node.

  5. Click the Configuration Manager button.

  6. In the Configuration Manager dialog box, you see a list of current build configurations. In the drop-down box, you can select New to add a new build configuration.

  7. In the New Configuration dialog box, you can enter a name for the new configuration and select an existing configuration to base it on. Typically, a configuration similar to Debug or Release can be chosen as the base.

  8. After clicking OK, the new build configuration will be added to the project or solution.

  9. You can switch to a new build configuration in the build configuration drop-down box, and then set different compilation options, preprocessing macros, etc. for it. As shown in [Figure-3]

Figure-3 Select custom build configuration options when compiling

This way, you can create and use custom build configurations. In a project involving many developers, the default debug and release configurations are for more general needs. This customized configuration is very useful when dealing with different build requirements and environments.

For example, if the code logic you are responsible for is mainly concentrated before the main window of the application is displayed, you may need to set a breakpoint at the entry of the Main function of the main process so that it can be linked to the debugging code in time. In your code, you can use macro definitions for conditional compilation to generate a popup message box under specific configuration options. This message box is displayed at runtime for certain configuration options and hidden for other configuration options such as Debug and Release. This practice makes it easier to identify and deal with problems during the development and debugging phases.

int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, LPTSTR lpCmdLine, int nCmdShow)
{
#ifdef _DEBUG_DEV
    ::MessageBox(NULL, lpCmdLine, _T("Please attach target process"), MB_OK | MB_TOPMOST | MB_SYSTEMMODAL);
#endif

For another example, when debugging a Debug version of a program generated by local compilation, sometimes you will encounter situations where some security checks cause the program to terminate. In order to continue debugging, we can use relevant macro definitions to bypass these checks. This way, we can ensure that the debugging process goes smoothly without being affected by some security restrictions.

ValidationResult IsValid(LPCWSTR updater, LPCWSTR installDir, LPCWSTR updateDir)
{
#ifdef _DEBUG_DEV
    return ValidationResult::Valid;
#endif
    // updater location check
    if (!CSecurityChecker::PathFileInProgramFiles(updater)) {
        return ValidationResult::PathIllegal;
    }

    // original file name check
    if (!CSecurityChecker::VerifyFileOriginalName(updater, SVC_COMMAND_UPDATE_PROCNAME)) {
        return ValidationResult::NotRealUpdater;
    }

    // signatrue check
    if (!CSecurityChecker::VerifyDigitalSign(updater)) {
        return ValidationResult::DigitalSignatureIllegal;
    }

    // safe version check
    if (!CSecurityChecker::VerifySafeVersion(updater)) {
        return ValidationResult::NotSafeVersion;
    }

    return ValidationResult::Valid;
}

Another example is when you want to replace a file in a certain default path when debugging related code. You can achieve this flexible operation by judging specific macro definitions. This way you can easily replace files during debugging without having to manually interfere with the default path.

#ifdef _DEBUG_DEV
    ::PathRemoveFileSpec(szSelfFilePath);
    ::PathAppend(szSelfFilePath, _T("debugTracer.dll"));
#endif

In short, the flexibility of preprocessing macros allows us to control the code for different development and debugging needs. The use of these macro definitions allows us to efficiently develop and debug code in different environments, improving the convenience and flexibility of development.