Parametric Macros in C
In the previous lesson we analyzed simple macros and saw their operating mechanism: textual substitution.
In C language, however, macros are also capable of accepting input parameters, just like a function. For this reason, they are called parametric macros or functional macros. Through parameter passing, we can create more flexible and powerful macros.
In this lesson we will see the syntax of parametric macros. Furthermore, we will see what are the advantages and also the disadvantages of their use compared to functions.
Parametric Macros
In the previous lesson we saw the syntax for defining macros in C language. We focused on simple macros and studied their operation.
Now we concentrate on the case of parametric macros, also called functional macros, whose main characteristic is that of being able to accept input parameters just like functions.
The general syntax for defining a parametric macro is shown below:
Parametric Macro in C
A Parametric Macro, in C language, is a macro that accepts input parameters. The general syntax for defining a parametric macro is the following:
#define MACRO_NAME(parameter1, parameter2, ..., parameterN) macro_body
Therefore, just like functions, a parametric macro can accept one or more parameters separated by commas. These parameters can be used within the macro body to generate customized code.
Let's see in detail. When the preprocessor encounters the definition of a macro with parameters, analogously to the case of simple macros, it stores the macro name and the associated body.
When, subsequently, the macro is invoked (in other words the preprocessor encounters an occurrence of it) in the form MACRO_NAME(Y1, Y2, ..., YN) the preprocessor replaces the macro name with the associated body replacing, furthermore, the formal parameters with the actual values passed as input.
The substantial difference compared to a function is that the arguments passed to a macro can be any tokens, that is, they do not have to respect a predefined syntax and much less be of a specific type.
Let's clarify with an example. Suppose we want to create a parametric macro that provides us with the maximum between two elements. We can write this macro like this:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
Looking at the macro definition, the number of parentheses present might seem strange. Actually there is a reason that we will see later. For now let's focus on using the macro.
Subsequently, after defining it, we can use the macro in our code in this way:
int a = 10;
int b = 20;
int maximum = MAX(a, b);
When the preprocessor encounters the invocation of the macro MAX(a, b) it will replace the macro name with the associated body and will replace the formal parameters (x) and (y) with the actual values a and b. Therefore, the previous code will become:
int maximum = ((a) > (b) ? (a) : (b));
This example shows a possible use of parametric macros as simple functions. In this case MAX accepts two parameters and returns the maximum between the two.
In the example, however, we limited ourselves to passing two integers as parameters. As already said, parametric macros can accept any type of token. Therefore, we can also pass more complex expressions as parameters. For example:
int maximum = MAX(a + 5, b * 2);
In this case the result, after substitution, will be:
int maximum = ((a + 5) > (b * 2) ? (a + 5) : (b * 2));
Or, we can also pass different types as parameters:
int maximum = MAX('c', 'b');
The result, after substitution, will be:
int maximum = (('c') > ('b') ? ('c') : ('b'));
In this case, the macro will return the character 'c' since the character 'c' has a greater ASCII value than 'b'.
Keep in mind that the macro substitution will not generate errors as long as the comparison operator > is defined on the type of the passed parameters. For numeric types, int, float and so on, and for char that we used above, the > operator is defined.
If, instead, two strings are passed as parameters, the preprocessor will generate an error since the comparison operator between strings is not defined.
All this derives from the fact, as we explained in the previous lesson, that macros are text substitutions and not true functions. Therefore, the preprocessor does not perform any check on the types of the passed parameters.
Attention to parentheses in the definition of parametric macros
Although the syntax for defining a parametric macro is very similar to that for defining a function, there is a detail to pay maximum attention to: you must never insert spaces between the macro name and the left parenthesis.
In fact, the following code presents problems:
/* INCORRECT CODE */
#define SUM (x, y) ((x) + (y))
In this case, in fact, the precompiler will consider as the macro body the string (x, y) ((x) + (y)) and not ((x) + (y)) as one would expect.
The consequence of this error is that, when the SUM macro is used, the precompiler will replace SUM(3, 4) with ((3) + (4))(3, 4) and not with ((3) + (4)) as one would expect.
The correct version is, instead, the following:
/* CORRECT CODE */
#define SUM(x, y) ((x) + (y))
Note well that, to define parametric macros, it is not necessary to specify a parameter list. For example, such a definition is perfectly legal:
#define PRINT_MESSAGE() (printf("Hello, how are you?"))
When we want to call this macro in our code, we can simply write:
PRINT_MESSAGE();
At first glance, the introduction of a syntax that allows defining a parametric macro without parameters may seem useless. We could have, for example, used a simple macro. Actually, it is a choice of consistency with the syntax of a function; from the user's point of view, it makes clear the fact that it is a macro that performs operations, like a function, if the syntax is similar to that of a function.
Parametric Macro without Parameters
A parametric macro can have an empty parameter list:
#define MACRO_NAME() macro_body
In this case, when you want to invoke the macro, it is still necessary to insert the empty round parentheses:
MACRO_NAME();
Examples
Let's see some examples of parametric macros to better fix the concepts.
Suppose we want to define a macro to check if a number is even. We can write the macro in this way:
#define EVEN_NUMBER(x) ((x) % 2 == 0)
In this case, the macro will return 1 if the number passed as a parameter is even, 0 otherwise.
Similarly, we can define a macro to check if a number is odd:
#define ODD_NUMBER(x) ((x) % 2 != 0)
We can define a macro for calculating the cube of a number:
#define CUBE(x) ((x) * (x) * (x))
Finally, we can define a macro for calculating the sum of the first n integers:
#define SUM_FIRST_N(n) ((n) * ((n) + 1) / 2)
Advantages of parametric macros
A parametric macro, we have seen, is very similar to a function. What we ask ourselves, now, is what are the advantages of using them compared to a normal function?
Let's see in detail.
Better performance can be obtained
Generally, the operation of invoking a function is not a zero-cost operation. There existed, and often still exists in modern systems, an overhead or additional cost associated with calling a function. Without going into technical detail, we can say that, in general, invoking a function involves allocating memory space for parameters, saving the return address, jumping to the function address and restoring the previous state at the end of the function.
All these operations, although fast, involve a cost that, in some cases, can be avoided. Parametric macros, being text substitutions, do not involve any additional cost. In fact, the body of parametric macros is substituted directly at the point where it is invoked. The final result is that the generated code is equivalent to what would be obtained by writing the code directly instead of the macro.
Let's return to the example of the MAX macro. The following code:
int maximum1 = MAX(a, b);
int maximum2 = MAX(c, d);
int maximum3 = MAX(e, f);
is transformed into the following code:
int maximum1 = ((a) > (b) ? (a) : (b));
int maximum2 = ((c) > (d) ? (c) : (d));
int maximum3 = ((e) > (f) ? (e) : (f));
In other words, it is as if we had repeated the code to calculate the maximum 3 times.
It is also true, however, that modern processors and modern compilers are able to optimize function calls in such a way as to minimize overhead. Therefore, in general, the use of parametric macros to obtain better performance is an advantage that has been reduced over time.
Furthermore, in the C99 standard, so-called inline functions were introduced that allow obtaining the same performance as parametric macros with the syntax of a function. We will see what this is about in a subsequent lesson.
Macros are generic
As we also mentioned above, the parameters of a macro do not have an associated type. The precompiler, unlike the case of a function, does not check whether the arguments passed to the macro are of integer type, floating point and so on.
Moreover, even in the definition of a parametric macro we did not specify the type of the parameters.
The consequence of this characteristic is that parametric macros can be used with any data type. The condition to respect, however, is that the resulting program after substitution is syntactically correct.
Let's return again to the example of MAX. We can use, as seen above, MAX with types int, float, double and char:
int maximum = MAX(10, 20);
float maximum_f = MAX(10.5, 20.5);
double maximum_d = MAX(10.5, 20.5);
char maximum_c = MAX('c', 'b');
We can do this by writing the macro definition once and only once. If we had wanted to use a function instead of the macro, we would have had to write a function for each data type:
int maximum_int(int x, int y) {
return x > y ? x : y;
}
float maximum_float(float x, float y) {
return x > y ? x : y;
}
double maximum_double(double x, double y) {
return x > y ? x : y;
}
char maximum_char(char x, char y) {
return x > y ? x : y;
}
The constraint is that after the textual substitution of the macro, the program continues to be valid. If we had used two arrays, for example, with the MAX macro we would have obtained a compilation error. This is because the > operator, used in the macro body, is not defined for arrays.
Disadvantages of parametric macros
We have seen the advantages of parametric macros. Let's see, now, what are the main disadvantages compared to functions.
The generated code is larger in size
Invoking a macro, in practice, consists of copying the macro body at the point where it is invoked. This means that, if the macro is invoked multiple times, its body will be copied and substituted in the code multiple times.
The consequence is that the size of the final code will be larger compared to the case of a function. Using a function, instead, the function code resides in a single point and is called every time the function is invoked.
Obviously, the problem increases as the number of invocations of a macro increases. And it explodes in the case where we use nested macros.
For example, we may want to find the maximum among three numbers. We can, in this case, use the same MAX macro twice in a nested manner:
int maximum = MAX(MAX(a, b), c);
In this case the code becomes:
int maximum = (((a) > (b) ? (a) : (b)) > (c) ? ((a) > (b) ? (a) : (b)) : (c));
This problem does not concern code readability at all. From the developer's point of view, in fact, the code is very clear and immediate. The problem is that the generated code is much larger compared to the case of a function. The final executable could be larger in terms of memory occupation.
It is also true, however, that nowadays the memory of a computer is no longer such a limited resource as to disfavor the use of parametric macros. However, especially when developing programs for embedded systems such as microcontrollers, code size is a critical factor.
The types of arguments are not checked
Although this aspect has already been covered in the advantages of parametric macros, it is good to emphasize that the fact that the types of arguments are not checked can lead to errors that are difficult to identify.
When a function is invoked, the compiler checks that the passed arguments correspond to the required type. If a function requires an int as input, the compiler will check that the passed argument is actually an int. In case it is not, the compiler tries to convert the passed argument to the required type. For example, if we try to pass a float instead of an int, the compiler will perform the conversion, but will still signal a warning to warn us of the loss of precision. If the conversion is not possible, the compiler will signal an error.
All this does not happen with the parameters of a macro. Neither types are checked nor conversions are performed.
This can lead to errors that are very difficult to find.
Pointers to macros do not exist
We will see in the next lessons that it is possible to define pointers to functions in C language. This concept is very powerful and flexible.
Since a macro is not a function, but represents a simple textual substitution mechanism, it is not possible to define a pointer to a macro.
Therefore, in these situations, only functions can be used exclusively.
The evaluation of a macro's arguments can occur multiple times
This is, perhaps, the most insidious aspect of using parametric macros.
To understand what this problem consists of, let's consider an example. Suppose we want to implement a function that calculates the cube of a number:
int cube(int x) {
return x * x * x;
}
Now, suppose that the code that invokes the cube function is done in this way:
int x = 10;
int y = cube(x++);
What happens in this example is shown below:
- The variable
xis initialized to10; - Before invoking the
cubefunction, the arguments are evaluated. In this case we have a single argumentx++. The++operator increments the value ofxby1and returns the previous value. Therefore, the value passed to thecubefunction is10, butxis incremented to11; - The
cubefunction calculates the cube of10and returns1000; - The returned value is assigned to the variable
y.
After the execution of this code snippet, as is reasonable to expect, the value of x will be 11 and the value of y will be 1000.
But what happens if we wanted to implement the same cube function as a parametric macro?
#define CUBE(x) ((x) * (x) * (x))
And if we wanted to invoke the CUBE macro in the same way?
int x = 10;
int y = CUBE(x++);
The result will be very different in this case. In fact, the code generated by the macro will be:
int x = 10;
int y = ((x++) * (x++) * (x++));
In this case, the variable x is incremented three times. This is because, as we said, a macro is a text substitution. Each occurrence of x in the macro body is replaced with the actual value of x at the time of substitution. Furthermore, the variable y will be equal to 10 * 11 * 12 = 1320.
The result x = 12 and y = 1320 is very different from what we expected!
Errors of this type can be very difficult to identify and solve. Also because invoking a function and invoking a parametric macro are, from a syntactic point of view, two identical operations.
The problem is even more serious since such errors can manifest themselves only under certain conditions. Returning to the example above, the error would not have manifested itself if we had not used the ++ operator inside the macro invocation. For example, the following code:
int x = 10;
int y = CUBE(x);
x++;
is completely identical to:
int x = 10;
int y = cube(x++);
Parametric macros and side effects
Since a parametric macro can evaluate its arguments more than once, it is recommended to avoid using complex expressions as arguments of a macro, especially if such expressions modify the state of the program.
Recurring patterns
We conclude this lesson with a last interesting use of parametric macros: the definition of recurring patterns.
One of the most convenient ways to use parametric macros is to replace repeated pieces of code.
Let's clarify with an example. Suppose that in our code we need to print double values multiple times with a certain precision. We could have a situation like this:
double a = 10.123456789;
printf("%.7f\n", a);
/* ... */
double b = 20.123456789;
printf("%.7f\n", b);
/* ... */
double c = 30.123456789;
printf("%.7f\n", c);
In this example, we have multiple double variables to print on screen with the same precision. Furthermore, the precision 7 is repeated multiple times. In case we wanted to change the precision, we would have to modify all the points where the double value is printed.
To avoid repeating the same code, we can define a parametric macro that accepts as a parameter the variable to print. For example:
#define PRINT_DOUBLE(x) printf("%.7f\n", x)
In this way, we can print the double values in this way:
double a = 10.123456789;
PRINT_DOUBLE(a);
/* ... */
double b = 20.123456789;
PRINT_DOUBLE(b);
/* ... */
double c = 30.123456789;
PRINT_DOUBLE(c);
If we wanted to change the precision with which the double values are printed, we can do so by modifying the macro definition only once.
The advantage of using a macro in this case is evident. Furthermore, the code is clearer and more readable. Therefore, when a repeated code pattern is found, it is possible to use a parametric macro to avoid repeating the same code multiple times.
In Summary
In this lesson we have seen how to define and use parametric macros in C language. We have seen that a parametric macro is a macro that accepts input parameters and that can be used in a similar way to a function.
The syntax for defining a parametric macro is the following:
#define MACRO_NAME(parameter1, parameter2, ..., parameterN) macro_body
We then focused on the advantages and disadvantages of parametric macros:
-
Advantages:
- Better performance can be obtained compared to functions;
- Macros are generic and can be used with any data type;
- Parametric macros without parameters can be defined;
- Recurring patterns can be defined.
-
Disadvantages:
- The generated code is larger in size compared to the case of a function;
- The types of arguments are not checked;
- Pointers to macros do not exist;
- The evaluation of a macro's arguments can occur multiple times.
In the next lesson we will study two very important operators in C language: the concatenation operator ## and the stringification operator #. These two operators can be used exclusively within the macro body.