6.7. Macros and Inline Functions

Time: 00:07:27 | Download: Large, Large (CC) Small | Streaming, Streaming (CC) | Slides (PDF)
Review

Macros and inline functions are very different programming constructs, but their purpose and behavior are similar. We first explored macros as one way of making a symbolic constant. In this section, we extend the macro syntax by adding parameters. The preprocessor expands macros by replacing one sequence of characters with another. Specifically, it replaces the macro name with the macro body. Parameterized macros are common in C programs but inline functions typically replace them in C++.

To understand macros and inline functions and why we might use them, we need to know how they differ from "normal" functions.

Regular, Non-Inline Function

An abstract representation of a function as a rectangle. The compiled program has only one copy of the function's instructions. Arrows illustrate that a function call jumps to the function's instructions while passing the function arguments to the function's parameters. Arrows also illustrate that when the function ends, control returns to the statement following the function call.
Typical function call. A compiled program has one instance of the function's machine instructions, which it executes whenever it calls the function. The call evaluates the arguments, a, b, c, and d, passes the results to the parameters x and y, and transfers control to the function's instructions. The program returns control to the instruction following the call when the function ends.
Parameterized MacroInline Function
The illustrations in the figure show the similarities and differences between a macro and an inline function. The two are similar in that code is duplicated each time either is used. The difference is how the arguments behave: macros copy the arguments (including any operators or other function calls) into the macro, whereas inline functions evaluate each argument and pass the result to the function.
Macro vs. inline function. The behavior of parameterized macros (a) and inline functions (b) are very similar, but there are some significant differences.
  1. The preprocessor expands macros in place by replacing the macro, f(x,y), with the statements in the macro body. The character strings represented by a and b (including any operators) replace x and y in the expansion. The expansions create one instance of the macro body for every occurrence of the macro "call" in the source code, eliminating control transfer to and from a function.
  2. Inline functions are also expanded in place, replacing the function calls, but the compiler component, not the preprocessor, performs the expansion. There is one copy of the function body for every function call in the source code. However, the program evaluates the arguments and passes the resulting values to the function parameters before the expansion occurs.

Parameterized Macros

A parameterized macro is an older programming construct used frequently in C but rarely in C++. Nevertheless, you must understand them because you may encounter them in older code, and understanding macros clarifies why their weaknesses drove language designers to create their modern replacements: inline functions.

#definesqr(x)x * x

int main()
{
	cout << sqr(2) << endl;

	return 0;
}

int main()
{
	cout << 2 * 2 << endl;

	return 0;
}

#define my_macro(x) \
	part1; \
	part2; \
	part3;




(a)(b)(c)
Parameterized macro example. From left to right, parameterized macros consist of the #define directive, an identifier or name, a comma-separated parameter list, the replacement text, and ends with a terminating newline character. The preprocessor expands the macro by replacing the name and argument list with the replacement text. During expansion, the preprocessor replaces the macro parameters embedded in the replacement text with the corresponding arguments; all other characters in the replacement text are copied verbatim to the preprocessor output.
  1. Parameterized macros have a rigid syntax (highlighted in pink). From left to right, they consist of the #define directive, an identifier or name, a comma-separated parameter list, the replacement text, and a terminating newline character. The spaces represented by the red boxes are required, consisting of one or more space or tab characters. The syntax does not permit spaces or tabs between the macro's name and the leftmost parenthesis. (The preprocessor doesn't report an error if there is a space, but the output is incorrect.) Modern preprocessors allow spaces between the parameters, but older ones do not. The macro invocation (light blue) looks like a function call but behaves differently.
  2. The preprocessor output: During expansion, the preprocessor replaces the parameter, x, with the macro argument, 2, while copying the multiplication operator and spaces verbatim.
  3. A macro definition ends with a newline character. Therefore, to create a macro spanning multiple lines, programmers must escape or "hide" any newline characters within the definition. The escape character, \, "hides" exactly one character, making it crucial not to have an invisible space or tab character following the backslash. Be cautious when placing comments or other characters in the replacement text, as the preprocessor includes in the macro expansion.
  Macro Invocation Expansion Result
(a) #define sqr(x) x * x sqr(2) 2 * 2 4
(b) sqr(2+3) 2 + 3 * 2 + 3 11
(c) #define sqr(x) ((x) * (x)) sqr(2+3) ((2 + 3) * (2 + 3)) 25
Correctable macro errors. Function arguments are expressions; the program evaluates them before calling the function and passes the results to the function's parameters. However, macros are simple textual replacements the preprocessor completes before the compiler component runs, which can lead to some unexpected results.
  1. If we invoke the sqr macro with a constant or simple variable argument, the macro expansion behaves as expected.
  2. However, invoking the macro with a more complex argument can produce unexpected results. The problem is that during the expansion, the characters 2+3 replace the parameter x as both the left- and right-hand operands of the multiplication operator. Multiplication has a higher precedence than addition, so 3*2 is evaluated before either addition operation.
  3. We can correct this problem with a generous application of grouping parentheses.

While parentheses can solve simple macro errors, there are others that no amount of parentheses can fix. Examine the following program and see if you can spot the error. You might find it helpful to review the auto increment and the conditional operators.

#include <iostream>
using namespace std;

#define min(x,y)	((x < y) ? (x) : (y))

int main()
{
	int	a = 5;
	int	b = 10;

	cout << min(a++, b++) << endl;
	cout << a << " " << b << endl;

	return 0;
}
6
7 11
(b)
cout << ((a++ < b++) ? (a++) : (b++)) << endl;
(a)(c)
Uncorrectable macro errors. We expect the macro to print the smallest of 5 and 10, which is 5. Furthermore, we expect the code to increment the 5 and 10 to 6 and 11, respectively, and then print those values with the second cout statement. The output demonstrates that the macro does not behave as expected.
  1. The macro definition (highlighted in pink) and invocation (highlighted in blue).
  2. The program output.
  3. It's easier to see the problem in the expanded text, which the program evaluates as follows:
    1. For a=5 and b=10, (a++ < b++) is true.
    2. As a side effect, it increments a and b, so a=6 and b=11.
    3. As the first expression is true, the value of the conditional operation is 6, but a second side effect, (a++), increments a again.
    4. Finally, the program prints the values saved in a and b.

Although appearing similar, macro invocations are not function calls. Programs evaluate function arguments before calling the function, but the preprocessor expands macro arguments before the compiler can generate code to evaluate them. Problems with macros, like the one illustrated here, spurred the development of improved techniques.

Inline Functions

Inline functions answer the problem but are almost anticlimactic in their simplicity and application compared to macros. Programmers create inline functions with the inline keyword.

#include <iostream>
using namespace std;

inline int min(int x, int y) { return (x < y) ? x : y; }	// (a)

int main()
{
	int	a = 5;
	int	b = 10;

	cout << min(a++, b++) << endl;				// (b)
	cout << a << " " << b << endl;

	return 0;
}
5
6 11

(c)

Inline function example. Unlike macros, inline functions are "true" C++ functions, consisting of a header and body. The body often requires fewer parentheses than a macro while still avoiding the problems of unwanted side effects.
  1. Inline functions begin with the inline keyword, but it is a suggestion that the compiler may choose to ignore. The header includes the return type, function name, and the parameter list. The program type checks the parameters and return value, completing any necessary type promotions. Inline functions are typically short and often written on a single line. Nevertheless, programmers can write them on multiple lines without escaping or "hiding" the newline characters.
  2. Although the program evaluates the arguments before calling the function, recall that the post-increment operator "uses" the saved value before incrementing it. So, the call passes 5 and 10 to the function and then increments the values held in a b.
  3. The correct and expected program output.

Functions Vs. Inline Functions

Functions vs. inline functions. The relative merits of traditional and inline functions derive from the differences between their respective implementations and operations. Specifically, the overhead of traditional function calls versus the size of repeated inline function code - another classic time/space trade-off. The value of inline functions is their execution speed. Although many steps are involved in a function call and return, most of those steps are small and execute quickly, so their overhead is relatively small. Nevertheless, there is some overhead.
  "Normal" Functions Macros and Inline Functions
Implementation The compiler creates one copy of the function's machine instructions. The compiler creates multiple copies of the function's machine instructions, one copy per call.
Operation The program creates a stack frame for each function call, saves the function's return address and arguments in it, jumps or transfers program control to the function, returns control and any return value to the call when the function ends, and deallocates the frame. The function's instructions replace the function calls, so the program executes them sequentially with the surrounding statements - there is no "jump" and return.
Appropriateness Appropriate for functions of any size. However, in the case of small functions, the call and return overhead may exceed the time for running the function's instructions. Appropriate for small functions, depending on the extensiveness of each statement, and some compilers disallow loops. Repeating the function's code at every call can create an unnecessarily large executable, especially for large functions.
Caveats The inline keyword is a suggestion that the compiler may ignore. Sometimes, a program needs a function's address, which inline functions don't have.

There isn't a universal metric separating "small" and "large" functions. However, most programmers agree that functions with three or four "small" statements are good candidates for inlining, while functions with seven or eight statements are too large.