Before any actual compiling occurs on a piece of program source code in Managed C++, it must first go through the preprocessor, just like in traditional C++. The purpose of the preprocessor is to prepare the program source code for compiling using a number of instructions called preprocessor directives.
These preprocessor directives provide the ability to do things such as include or exclude code based on conditions, define constants, and so on. All of the directives are prefixed with the # symbol (variously called pound, number sign, and hash), which makes them stand out from the rest of the program source code. Table 4-1 shows a complete set of all preprocessor directives for Managed C++.
DIRECTIVE | DESCRIPTION |
---|---|
#define #undef | Defines or undefines a meaningful name to a constant or macro in your program. |
#if | Allows for conditional compilation of program source code. |
#ifdef | |
#ifndef | |
#elif | |
#else | |
#endif | |
#error | Intended to allow you to generate a diagnostic error when something goes wrong in the preprocessor stage. |
#include | Provides header file insertion. |
#line | Redefines the compiler's internally stored line number and filename with the provided line number and filename. |
#pragma | Provides machine/operating system-specific features while retaining compatibility with C++. Most likely, the only #pragma directives that you will encounter in managed C++ are once, which causes an include file to be only included once, and managed and unmanaged, which allow for function-level control of compiling functions as managed or unmanaged. |
#using | Imports .NET assembly metadata into program source code using Managed C++. |
The four directives that you'll most likely deal with using Managed C++ are the defining, conditional, include, and using directives. Other than the #using directive, there's no difference between Managed C++ and traditional C++ when it comes to the available processor directives, though the #import and many #pragma directives don't make sense and won't be used with Managed C++. This is appropriate, as Managed C++ wasn't designed to change how C++ works; instead, it's supposed to extend C++ to work with .NET.
By convention, preprocessor directives are placed near the top of the source code. In actuality, other than a select few exceptions (the #using preprocessor directive comes to mind as it needs global scope) you can place a preprocessor directive on its own line almost anywhere in the code—basically where it makes sense. The #define declarative, for instance, just needs to be placed before it is used.
The #define directive is used to execute a macro substitution of one piece of text for another. Here are the three basic syntaxes for implementing #define:
#define identifier #define identifier token-string #define identifier(parameter1,..., parameterN) token-string
The first syntax defines the existence of a symbol. The second syntax allows for the substitution of text identified by the identifier with the following token-string. The third syntax provides the same functionality as the second, plus the passed parameters are placed within the token-string. Listing 4-1 shows the source code before it has been passed through the preprocessor.
Listing 4-1: Original #defined Code
#define DISAPPEARS #define ONE 1 #define TWO 2 #define POW2(x) (x)*(x) Int32 main () { Console::Write(S"The following symbol disappears->" DISAPPEARS); Console::WriteLine(S"<-"); Int32 x = TWO; Int32 y = POW2(x + ONE); Console::WriteLine(y); return 0; }
Listing 4-2 shows the source code after it has passed through the preprocessor. Notice that all identifiers have been substituted with their token-string, or lack of token-string in the case of the DISAPPEARS identifier.
Listing 4-2: Processed #defined Code
Int32 main () { Console::Write(S"The following symbol disappears->" ); Console::WriteLine(S"<-"); Int32 x = 2; Int32 y = (x + 1)*(x + 1); Console::WriteLine(y.ToString()); return 0; }
The #undef directive's purpose is to remove a previously defined symbol. Unlike #define, there is only one syntax:
#undef identifier
The #undef directive undefines symbols that have been previously defined using the #define directive or the /D compile time switch. If the symbol was never defined, then the #undef directive will be ignored by the preprocessor. If you forget to #undef a symbol before you #define it again, the compiler will generate a warning but will let you continue. It is probably a good idea whenever you see this warning to #undef the variable just before you #define it again to get rid of the warning, but there is nothing saying you have to.
Another approach that you can use to get rid of the warning for an already assigned symbol is to use the #pragma push_macro() and #pragma pop_macro() directives in conjunction with the #undef and #define directives. With this approach, the value of the symbol is stored so that it can be reassigned later after the application no longer needs the new symbol definition. Here is a simple example:
#define MY_SYMBOL S"Original" #pragma push_macro("MY_SYMBOL") #undef MY_SYMBOL #define MY_SYMBOL S"New Value" Console::WriteLine(MY_SYMBOL); #pragma pop_macro("MY_SYMBOL") Console::WriteLine(MY_SYMBOL);
Conditional directives provide the ability to selectively compile various pieces of a program. They work in a similar manner to the "if" flow control construct covered in Chapter 2. The big difference is that instead of not executing a particular section of code, now it will not be compiled.
The basic syntax for conditional directives is as follows:
#if constant-expression // code #elif constant-expression // code #else // code #endif
Something like the "if" flow control construct, the first #if or #elif constant-expression that evaluates to nonzero or true will have its body of code compiled. If none of the constant-expressions evaluates to true, then the #else body of code is compiled.
Only one of the blocks of code will be compiled, depending on the result of the constant-expressions. The constant-expressions can be any combination of symbols, integer constants, character constants, and preprocessor operators (see Table 4-2).
OPERATOR | DESCRIPTION |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulus |
& | Bitwise AND |
| | Bitwise OR |
^ | Bitwise XOR |
&& | Logical AND |
|| | Logical OR |
<< | Left shift |
>> | Right shift |
== | Equality |
! = | Inequality |
< | Less than |
> | Greater than |
<= | Less than or equal to |
>= | Greater than or equal to |
defined | Symbol is defined |
!defined | Symbol is not defined |
Though usually quite simple, an expression can become quite complex, as the following example suggests:
#define ONE 1 #define TWO 2 #define THREE 3 #if ((ONE & THREE) && (TWO <= 2)) || defined FOUR Console::WriteLine(S"IF"); #else Console::WriteLine(S"ELSE"); #endif
The #if directive has two special preprocessor operators called defined and !defined. The first evaluates to true on the existence of the identified symbol. The second, obviously, evaluates to true if the identified symbol does not exist. To simplify the syntax, and because the defined and !defined operators are the most commonly used preprocessor operators with the #if directive, special versions of the directive were created: #ifdef and #ifndef.
These two directives are equivalent:
#if defined symbol #ifdef symbol
and so are these two:
#if !defined symbol #ifndef symbol
The #include directive causes the compiler to insert a piece of code into another piece of code. The most common usage of the #include directive is to place header files containing type definitions at the top of a piece of source file to ensure that the types are defined before they are used.
There are two different #include directive syntaxes for including a file in a source. The first uses angle brackets (<>) to enclose the file's path and the second uses double quotes (""):
#include <file-path-spec> #include "file-path-spec" #include <windows.h> #include "myclassdef.h" #include "c:/myincludes/myclassdef.h"
Each directive syntax causes the replacement of that directive by the entire contents of its specified file. The difference when processing the two syntaxes is the order that files are searched for when a path is not specified. If the file's path is specified, then no search is done and the file is expected to be at the location specified by the path. One major drawback is that the path cannot be a network path (per the Universal Naming Convention [UNC]). In a corporate, multideveloper site, this inability could be quite a nuisance or possibly even crippling. Table 4-3 summarizes the differences between the angle bracket and double quote syntax search methods when no path is specified.
SYNTAX FORM | SEARCH METHOD |
---|---|
#include <...> | Check for files along the path specified by the /I compiler option and then along paths specified by the INCLUDE environment variable. |
#include "..." | Check for files in the same directory of the file that contains the #include statement, then along the path specified by the /I compiler option, and, finally, along paths specified by the INCLUDE environment variable. |
Caution | Though the C++ compiler supports the INCLUDE environment variable, Visual Studio .NET does not. |
I covered the #using directive in passing in Chapter 2. Conceptually, there isn't much to it, as all it does is import metadata from within a .NET assembly and place it within the Managed C++ source file. This functionality is very similar to the #include directive discussed in the previous section.
The syntax of the #using directive purposely resembles that of the #include directive. This makes sense, as the #using directive's function resembles that of the #include directive. The only difference in the syntax between #using and #include is that you replace "include" with "using":
#using <assembly-path-spec> #using "assembly-path-spec" #using <mscorlib.dll> #using "myassembly.dll" #using <DEBUG/myassembly.dll>
There is no difference between using quotes and angle brackets as there is with the #include directive. Because this is the case, you will almost always see angle brackets used with #using directives. With either the double quote method or the angle bracket method, the compiler searches for the assembly using the following path:
The path specified by the #using directive
The current directory
The .NET Framework system directory
Directories added with the /AI compiler option
Directories in the LIBPATH environment variable
Caution | The #using directive is only used to help the compiler and the Visual Studio .NET IDE find the assembly. It does not tell the CLR where to find it. To run the application, you must still place the assembly in a location where the CLR knows to find it. |
It should be noted that the keyword using and the preprocessor directive #using are different. The using keyword enables coding without the need of explicit qualifications. Basically, the using keyword says, "Whenever a class or variable does not exist in the current scope, check the scope of the namespace specified by the using statement and, if it is there, use it just like it is part of the current scope."