Macros


Macros are named code segments that are substituted to their names each time they are encountered in a program.

Macros offer a powerful way to extend the language and create reusable code.
One reason macros are used is performance.
They are a way of eliminating procedure call overhead because they are always expanded in-line.
There is no alternative for this in FreeBASIC because it does not support inline procedures.

Macros definition
The preprocessor can, during the text replacement mechanism, use parameters supplied to the identifier to be replaced.
These parameters are then replaced without modification in the replacement text.
The replacement text is then called macro.

The syntax to define a macro is as follows (see 'Preprocessor commands'):
- one-line macro:
#define identifier([parameters]) body
parameters turn a define into a function-like macro, allowing text arguments to be passed to the macro.
identifier should be followed by the opening parentheses (() immediately without any white-space in between, otherwise the compiler will treat it as part of the body.
- multi-line macro:
#macro identifier([parameters])
body
#endmacro
#macro is the multi-line version of #define.

The # stringize operator can be used on macro parameters to turn them into string literals, and the ## concatenate operator can be used to merge tokens together.
(see 'Preprocessor Operators')

Defines and macros are scoped (they are only visible in the scope they were defined in).
Namespaces on the other hand do not have any effect on the visibility of the defines and macros.

The mechanism of macros allows to do the equivalent of general procedures, which work for all types.
However, care must be taken that parameters passed to a macro are evaluated by it each time they are used in the macro definition. This can cause performance issues or, worse, cause unwanted edge effects.

Parentheses should always be placed around the parameters of the macro:
- Indeed, these parameters can be compound expressions, which must be computed completely before being used in the macro.
- Parentheses force this calculation.
- If they are not set, priority rules can generate a logic error in the macro itself.
Similarly, macros that return a value should be surrounded by parentheses, in order to force their complete evaluation before using them in another expression.

Example of incorrect and correct macros:
#define MUL1(x, y) x * y              '' incorect macro  (parameters must be enclosed in parentheses)
#define MUL2(x, y) ( x ) * ( y )      '' incorrect macro (and returned result must also be in parentheses)
#define MUL3(x, y) ( ( x ) * ( y ) )  '' correct macro

Print MUL1(5-3, 1+2)^2  '' 6  (incorrect result)
Print MUL2(5-3, 1+2)^2  '' 18 (incorrect result)
Print MUL3(5-3, 1+2)^2  '' 36 (correct result)

Sleep
Thus, the parentheses ensure a consistent behavior of the macro (parentheses can add to macro definitions, but they are absolutely necessary).

Macros debug
Using macros can be extremely unsafe and they hide a lot of pitfalls which are very hard to find.
Procedures give type checking and scoping, but macros just substitute the passed argument.
Another disadvantage of the macro is the size of the program. The reason is, the pre-processor will replace all the macros in the program by its real definition prior to the compilation process of the program.

Looking only at the source code file, the only way to find out what the problem is to look at the definition of the macro and try to understand what happened.
The most common error when using macros is the unbalanced open parentheses (inducing error at compile-time).
Another is to forget putting parentheses around arguments (or returned result if exists) in macro definitions. That can cause some pretty nasty side effects because of operator precedence (inducing error at compile-time or bug at run-time).

When the compiler detects an error inside a macro (after expanding), it provides a rustic error message containing only:
- the line number where is the call of the macro,
- the error type,
- the text of the call (of the macro).

When the error is not obvious (type of error reported blurred), the only solution (with only the call line number) is presently to iteratively execute the following 5 steps until correction successful:
Do
1. call fbc on the source file, but with the '-pp' compile option (fbc command emitting only the pre-processed input file, without compiling),
2. recover the pre-processed file,
3. edit and compile directly from this pre-processed file,
4. analyze the error, understand it, correct it, and compile again the pre-processed file thus modified,
5. postpone the equivalent correction in the concerned macro body of the original source file.
Loop

Example of error on a short code:
  • Source file (*.bas):
  • #macro FIRST(array, Operator)
      For index As Integer = LBound(array) To UBound(array) - 1
        Print " " Operator array(index) Operator ",";
      Next index
      Print array(UBound(array))
    #endmacro

    #macro Second(array)
      Print #array + ":"
      FIRST(array, +)
    #endmacro

    Dim As String test1(0 To ... ) => {"First", "Second", "Third"}
    Dim As Integer test2(0 To ...) => {1, 2 ,3}

    Second(test1)
    Print
    Second(test2)

    Sleep
    Compiler output:
    ...\FBIDETEMP.bas(18) error 20: Type mismatch, found '+' in 'SECOND(test2)'

  • Pre-processed file (*.pp.bas):
  • Dim As String test1(0 To ... ) => {"First", "Second", "Third"}
    Dim As Integer test2(0 To ...) => {1, 2 ,3}

     Print $"test1" + ":"
     For index As Integer =LBound(test1) To UBound(test1) -1
     Print " " + test1(index) + ",";
     Next index
     Print test1(UBound(test1))
    Print
     Print $"test2" + ":"
     For index As Integer =LBound(test2) To UBound(test2) -1
     Print " " + test2(index) + ",";
     Next index
     Print test2(UBound(test2))

    Sleep
    Compiler output:
    ...\FBIDETEMP.bas(14) error 20: Type mismatch, found '+' in 'Print " " + test2(index) + ",";'

  • Example macro correction in source code (*.bas):
  • #macro FIRST(array, Operator)
      For index As Integer = LBound(array) To UBound(array) - 1
        Print " " Operator array(index) Operator ",";
      Next index
      Print array(UBound(array))
    #endmacro

    #macro Second(array)
      Print #array + ":"
      FIRST(array, &)     ''corrected line ("&" instead of "+")
    #endmacro

    Dim As String test1(0 To ... ) => {"First", "Second", "Third"}
    Dim As Integer test2(0 To ...) => {1, 2 ,3}

    Second(test1)
    Print
    Second(test2)

    Sleep
    Output:
    test1:
     First, Second,Third
    
    test2:
     1, 2, 3

Note: Another solution could be a more detailed error message from compiler, relating to the call of the macro (proposal of improved error message already filled in in a feature request).

Variadic macros
Using an ellipsis "..." (3 dots) behind the last parameter in a #macro or #define declaration allows creation of a variadic macro:
#macro identifier([parameters,] variadic_parameter...)
body
#endmacro
or:
#define identifier([parameters,] variadic_parameter...) body

So, it is possible to pass any number of arguments through the variadic_parameter, which can be used in the body as if it was a normal macro parameter.
During macro expansion each occurrence of the variadic_parameter in the macro replacement list is replaced by the passed arguments. The variadic_parameter will expand to the full list of arguments passed to it, including commas, and can also be completely empty.

No direct means is provided to access individual arguments in the variable argument list, nor to find out how many were passed.
To distinguish between the different arguments passed by the variadic_parameter, one can first convert the variadic_parameter to a string literal using the # stringize operator, then differentiate in this string literal (#variadic_parameter) each passed argument by locating the separators (usually a comma).

Examples
Example with one-line and multi-line macros:
#define MIN(x, y) IIf( ( x ) < ( y ), x, y )      '' maximum function-like macro
#define MAX(x, y) IIf( ( x ) > ( y ), x, y )      '' minimum function-like macro
#define MUL(x, y) ( ( x ) * ( y ) )               '' multiply function-like macro

#macro REV_STR(str_dest, str_src)                 '' reverse-string sub-like macro
    For i As Integer = Len(str_src) To 1 Step -1
        str_dest &= Mid(str_src, i, 1)
    Next I
#endmacro

Print MIN(5 - 3, 1 + 2)    '' 2
Print MAX(5 - 3, 1 + 2)    '' 3
Print MUL(5 - 3, 1 + 2)^2  '' 36

Dim As String s
REV_STR(s, "CISABeerF")  '' FreeBASIC
Print s

Sleep

Example with variadic macro:
' macro with a variadic parameter which can contain several sub-parameters:
'   To distinguish between the different arguments passed by variadic_parameter,
'   you can first convert variadic_parameter to a string using the Operator # (Preprocessor Stringize),
'   then differentiate in this string (#variadic_parameter) each passed argument by locating the separators (usually a comma).

#macro average(result, arg...)
    Scope
        Dim As String s = #arg
        If s <> "" Then
            result = 0
            Dim As Integer n
            Do
                Dim As Integer k = InStr(1, s, ",")
                If k = 0 Then
                    result += Val(s)
                    result = result / (n + 1)
                    Exit Do
                End If
                result += Val(Left(s, k - 1))
                n += 1
                s = Mid(s, k + 1)
            Loop
        End If
    End Scope
#endmacro

Dim As Double result
average(result, 1, 2, 3, 4, 5, 6)
Print result

' Output : 3.5

See also
Back to Programmer's Guide
Valid XHTML :: Valid CSS: :: Powered by WikkaWiki



sf.net phatcode