Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
CPlusPlusNotesForProfessionals.pdf
Скачиваний:
47
Добавлен:
20.05.2023
Размер:
5.11 Mб
Скачать

Chapter 75: Preprocessor

The C preprocessor is a simple text parser/replacer that is run before the actual compilation of the code. Used to extend and ease the use of the C (and later C++) language, it can be used for:

a.Including other files using #include

b.Define a text-replacement macro using #define

c.Conditional Compilation using#if #ifdef

d.Platform/Compiler specific logic (as an extension of conditional compilation)

Section 75.1: Include Guards

A header file may be included by other header files. A source file (compilation unit) that includes multiple headers may therefore, indirectly, include some headers more than once. If such a header file that is included more than once contains definitions, the compiler (after preprocessing) detects a violation of the One Definition Rule (e.g. §3.2 of the 2003 C++ standard) and therefore issues a diagnostic and compilation fails.

Multiple inclusion is prevented using "include guards", which are sometimes also known as header guards or macro guards. These are implemented using the preprocessor #define, #ifndef, #endif directives.

e.g.

// Foo.h

#ifndef FOO_H_INCLUDED #define FOO_H_INCLUDED

class Foo

// a class definition

{

 

};

 

#endif

 

The key advantage of using include guards is that they will work with all standard-compliant compilers and preprocessors.

However, include guards also cause some problems for developers, as it is necessary to ensure the macros are unique within all headers used in a project. Specifically, if two (or more) headers use FOO_H_INCLUDED as their include guard, the first of those headers included in a compilation unit will e ectively prevent the others from being included. Particular challenges are introduced if a project uses a number of third-party libraries with header files that happen to use include guards in common.

It is also necessary to ensure that the macros used in include guards do not conflict with any other macros defined in header files.

Most C++ implementations also support the #pragma once directive which ensures the file is only included once within a single compilation. This is a de facto standard directive, but it is not part of any ISO C++ standard. For example:

// Foo.h #pragma once

class Foo

GoalKicker.com – C++ Notes for Professionals

399

{

};

While #pragma once avoids some problems associated with include guards, a #pragma - by definition in the standards - is inherently a compiler-specific hook, and will be silently ignored by compilers that don't support it. Projects which use #pragma once are more di cult to port to compilers that don't support it.

A number of coding guidelines and assurance standards for C++ specifically discourage any use of the preprocessor other than to #include header files or for the purposes of placing include guards in headers.

Section 75.2: Conditional logic and cross-platform handling

In a nutshell, conditional pre-processing logic is about making code-logic available or unavailable for compilation using macro definitions.

Three prominent use-cases are:

di erent 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 di erent 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

GoalKicker.com – C++ Notes for Professionals

400

<< "--------

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

Section 75.3: X-macros

An idiomatic technique for generating repeating code structures at compile time.

An X-macro consists of two parts: the list, and the execution of the list.

Example:

#define LIST \ X(dog) \

GoalKicker.com – C++ Notes for Professionals

401

X(cat) \

X(racoon)

//class Animal {

//public:

//void say();

//};

#define X(name) Animal name; LIST

#undef X

int main() {

#define X(name) name.say(); LIST

#undef X

return 0;

}

which is expanded by the preprocessor into the following:

Animal dog;

Animal cat;

Animal racoon;

int main() { dog.say(); cat.say(); racoon.say();

return 0;

}

As lists become bigger (let's say, more than 100 elements), this technique helps to avoid excessive copy-pasting. Source: https://en.wikipedia.org/wiki/X_Macro

See also: X-macros

If defining a seamingly irrelevant X before using LIST is not to your liking, you can pass a macro name as an argument as well:

#define LIST(MACRO) \ MACRO(dog) \ MACRO(cat) \ MACRO(racoon)

Now, you explicitly specify which macro should be used when expanding the list, e.g.

#define FORWARD_DECLARE_ANIMAL(name) Animal name; LIST(FORWARD_DECLARE_ANIMAL)

If each invocation of the MACRO should take additional parameters - constant with respect to the list, variadic macros can be used

//a walkaround for Visual studio #define EXPAND(x) x

GoalKicker.com – C++ Notes for Professionals

402