Special Interest Group on C++
This post describes some macros I routinely use when experimenting with code. It provides a step-by-step exposition of the use cases and the design leading to the use of variadic macros to satisfy requirements. In the process, the post also touches on the decision (and a need) to use macros instead of function templates.
There is decidedly not much to the macros, but I chose to describe them because there is much educational value due to some tricky issues that need to be addressed in a practical solution.
Update July 30, 2020: I added a section about printing the text of an expression containing commas. I also added four exercises related to the new section.
Many code examples in this blog (and elsewhere) often illustrate concepts by printing
the results of expressions along with a suitable heading. Listing A shows four such lines
of code and their outputs. For simplicity, the listing omits variable declarations. Also,
Line 2 of the listing intentionally uses NULL
instead of nullptr
so that the code is
consistent with later examples that illustrate compatibility of the macros with C++98.
Each of the four lines of code in Listing A represents a different use case:
std::strlen(z)
).typename of a:
) and then value of
another expression (typeid(a).name()
).sv1.data() != NULL
) as heading and then print the
value of that expression. This is the most common use case.duration:
), then the value of another
expression (elapsed
), and then the value of a “tail” expression (s
).std::cout << std::strlen(z) << '\n';
std::cout << "typename of a: " << typeid(a).name() << '\n';
std::cout << "s.data() != NULL: " << (s.data() != NULL) << '\n';
std::cout << "duration: " << elapsed << "s\n";
5
typename of a: A10_i
s.data() != NULL: true
duration: 0.0009001s
The issue with using the kind of code in Listing A is that it translates to a lot of
code. Also, in Use Case 3, it is easy to forget editing the expression in the heading
when the expression changes. For example, in Listing A, it is easy to forget to change
the heading text if the expression to evaluate is changed to use nullptr
instead of
NULL
.
Use Cases 1, 2, and 4 are easily implemented with function templates, but that approach produces a lot of code; not to mention many template instantiations. Further, Use Case 3 should be ideally satisfied by automatically generating the heading from the expression itself, and that is possible only with a macro; not with a function (template).
Because Use Case 3 is satisfied only with a macro, and because the other cases are also easily satisfied using 1-line macros, it is better to implement all cases with just macros. Plus, the resulting macros can be easily pasted into any code where they are needed. In contrast, function templates would be quite long and not as easy to reuse (but they do provide better type safety; see Exercises 5 and 6).
Listing B shows an initial set of function-like macros to collectively implement the
four use cases identified: one macro per use case. The listing also shows the macros
being used to print the same information printed in Listing A. The main
function
intentionally uses C++98 features to illustrate that the macros work that far back.
#define PRINTLN(x) std::cout << (x) << '\n'
#define PRINT_HXLN(h,x) std::cout << (h) << ": " << (x) << '\n'
#define PRINT_XLN(x) std::cout << #x << ": " << (x) << '\n'
#define PRINT_HXTLN(h,x,t) std::cout << (h) << ": " << (x) << (t) << '\n'
int main() {
std::cout << std::boolalpha;
char z[] = "hello"; // no UIS in C++98
int a[10];
std::string s;
double elapsed = 0.0009001;
// the following four lines match the four code lines in Listing A
PRINTLN(std::strlen(z));
PRINT_HXLN("typename of a", typeid(a).name());
PRINT_XLN(s.data() != NULL); // no constant nullptr in C++98
PRINT_HXTLN("duration", elapsed, 's');
}
Here is a brief note on each of the four macros in Listing B:
PRINTLN(x) std::cout << (x) << '\n'
: This macro simply prints the value of
expression x
.
PRINT_HXLN(h,x) std::cout << (h) << ": " << (x) << '\n'
: This macro prints the
value of expression h
as heading and then prints the value of expression x
.
PRINT_XLN(x) std::cout << #x << ": " << (x) << '\n'
: This macro prints the text of
expression x
as heading and then prints the value of that expression. It uses the
# operator+
to “stringify” the expression supplied.
PRINT_HXTLN(h,x,t) std::cout << (h) << ": " << (x) << (t) << '\n'
: This macro
prints the value of expression h
as heading, then prints the value of expression
x
, and then prints the value of the tail expression t
.
The following general points apply to all the macros developed:
As the code linked in Listing B shows, each macro has two variations: one that inserts
a new line after inserting values (as shown in Listing B); and one that does not
insert a new line. The variant that inserts a new line has the LN
suffix in its name;
the other variant does not have the LN
prefix. For example, PRINTLN
and PRINT
. In
all, a total of eight macros are developed.
Every expression received is parenthesized to ensure the expression is evaluated fully before inserting the result into the output stream.
Each macro expands to an expression whose value is a reference to std::cout
. Thus,
other insertions can be chained with each invocation of the macro. For example, one
could write the following statement:
PRINT("hello") << " world";
Because each macro expands to an expression, a semi-colon is necessary to treat each
invocation as a statement (as shown in the main
function of Listing B). This need is
not a limitation imposed by the macros developed, but a C++ requirement to change any
function-call expression to a statement.
The eight macros at the link included in Listing B can be modularized so as to increase reuse among the macros. Listing C shows the result of modularization. The following points are worth noting about the modularized macros:
The modularization is possible because the macros expand to expressions instead of statements.
The modularization of PRINT_XLN
follows a different pattern than what the other
new-line inserting macros follow. Exercise 3 explores the reason for the
difference.
#define PRINT(x) std::cout << (x)
#define PRINT_HX(h,x) PRINT(h) << ": " << (x)
#define PRINT_X(x) PRINT_HX(#x,x)
#define PRINT_HXT(h,x,t) PRINT_HX(h,x) << (t)
#define PRINTLN(x) PRINT(x) << '\n'
#define PRINT_HXLN(h,x) PRINT_HX(h,x) << '\n'
#define PRINT_XLN(x) PRINT_HXLN(#x,x)
#define PRINT_HXTLN(h,x,t) PRINT_HXT(h,x,t) << '\n'
The macros PRINT_X
and PRINT_XLN
in Listing C do not handle argument expressions
involving commas, specifically if the commas are not inside parentheses. The code segment
below shows example expressions with and without issues:
PRINT_XLN(f(1,2)); // OK: comma interpreted correctly
PRINT_XLN(1,2); // error: comma in the argument is not inside parens
PRINT_XLN(std::array<int,2>().size()); // error: same as Line 2
The obvious solution is to place offending expressions inside parentheses, thus forcing the pre-processor to treat the parenthesized expression as one argument. However, that approach causes the printed heading to include parentheses, which is likely not desired.
A better solution is to place the offending expression in parentheses but not print
the parentheses in the heading. Listing D shows this solution using two new macros
PRINT_PX
and PRINT_PXLN
and a function trim_print
. (“PX” stands for parenthesized
expression.)
Macros PRINT_PX
and PRINT_PXLN
simply invoke function trim_print
with the text of
the expression. Function trim_print
receives a constant C-string
(because #x
in the calling macro is guaranteed to be a C-string literal). It assumes
the length of C-string is at least two and prints everything in the C-string except the
first and last character.
std::ostream& trim_print(const char* z) {
for(++z; *(z+1); std::cout << *z, ++z);
return std::cout;
}
#define PRINT_PX(x) trim_print(#x) << ": " << (x)
#define PRINT_PXLN(x) trim_print(#x) << ": " << (x) << '\n';
int main() {
PRINT_XLN(f(1,2)); // continue to use PRINT_XLN
PRINT_PXLN((1,2)); // parenthesize and use custom macro
PRINT_PXLN((std::array<int,2>().size())); // parenthesize and use custom macro
}
The macros presented thus far send output only to std::cout
. However, sometimes it may
be necessary to optionally send output to a different stream. This feature can be
supported using variadic macros which are function-like macros with variable number of
arguments.
A variadic macro+ is denoted by
placing an ellipsis (...
) after all the fixed parameters in the macro definition. (It
is OK for a macro to have no fixed parameters and receive only variable number of
arguments.) The token __VA_ARGS__
in the replacement list of a variadic macro
represents the actual arguments passed for the variadic parameter.
The following key information applies to variadic macros:
Variadic macros were introduced in C++11.
Until C++20, the ellipsis in a macro definition stands for “one or more arguments”.
That is, at least one argument must be supplied to the variadic parameter. For example,
invoking the variadic macro F(x,...)
requires at least two arguments: one for the
fixed parameter x
; another for the variadic parameter.
Since C++20, the ellipsis in a macro definition stands for “zero or more arguments”.
That is, arguments to the variadic parameter are optional. For example, the variadic
macro F(x,...)
may be invoked with just one argument (for the fixed parameter x
).
Until C++20, both GCC and clang generate a warning if the -pedantic
compiler
option is set and a variadic macro is invoked without any argument for the variable
parameter. However, unless warnings are treated as errors, the code compiles
successfully.
Visual Studio 2019 Version 16.5.5 does not produce warnings if a variadic macro is invoked without any argument for the variadic parameter. (Visual Studio documentation does not list any complier option to control warnings in this situation.)
Listing E shows the use of variadic macros to optionally set the output stream. These macros are enabled by:
the modular organization of macros (shown in Listing C);
function ostream
which simply returns the stream reference it receives, returning
std::cout
by default if no argument is supplied; and
passing __VA_ARGS__
as the argument to function ostream
, which has the effect of
using std::cout
if __VA_ARGS__
is empty, and using __VA_ARGS__
as the stream if
an argument is supplied.
inline std::ostream& ostream(std::ostream& o = std::cout) { return o; }
inline std::ostream& trim_print(const char* z, std::ostream& o = std::cout) {
for(++z; *(z+1); o << *z, ++z);
return o;
}
#define PRINT(x,...) ostream(__VA_ARGS__) << (x)
#define PRINT_HX(h,x,...) PRINT(h,__VA_ARGS__) << ": " << (x)
#define PRINT_X(x,...) PRINT_HX(#x,x,__VA_ARGS__)
#define PRINT_PX(x,...) trim_print(#x,ostream(__VA_ARGS__)) << ": " << (x)
#define PRINT_HXT(h,x,t,...) PRINT_HX(h,x,__VA_ARGS__) << (t)
#define PRINTLN(x,...) PRINT(x,__VA_ARGS__) << '\n'
#define PRINT_HXLN(h,x,...) PRINT_HX(h,x,__VA_ARGS__) << '\n'
#define PRINT_XLN(x,...) PRINT_HXLN(#x,x,__VA_ARGS__)
#define PRINT_PXLN(x,...) trim_print(#x,ostream(__VA_ARGS__)) << ": " << (x) << '\n';
#define PRINT_HXTLN(h,x,t,...) PRINT_HXT(h,x,t,__VA_ARGS__) << '\n'
int main() {
PRINTLN(std::strlen(z)); // default std::cout
std::ostringstream str_out;
PRINT_XLN(s.data() != NULL, str_out); // send to string stream
PRINTLN(str_out.str()); // default std::cout
PRINT_HXTLN("duration", elapsed, 's', std::cerr); // send to std::cerr
}
The use of printing for diagnosis and illustration is fairly common, and macros are a convenient means to meet some of those requirements. Although function templates could be used instead of macros for the most part, only a macro can automatically generate the text of the expression from an expression. Also, using macros avoids compile-time template instantiations, but macros do not provide the type safety that templates do. With that said, macros are still the preferred approach due to their simplicity and ease of reuse.
Here are a few things to keep in mind when using the macros presented:
To use the macros, simply copy and paste the macros to an appropriate location in your own source file (or include file). When copying, please include a link to this post so people can follow the rationale for the choices, and also to acknowledge the source.
Use the macros in Listing D if you print only to std::cout
so that you do not
unnecessarily use variadic macros in your code: variadic macros without type checking
can introduce errors. (See Exercises 5 and 6.)
Use the macros in Listing E if in the same program you need to print to different streams. Strictly speaking, invoking these macros without specifying an output stream requires C++20, but the macros works just fine in GCC, clang, and MSVC, even if the compiler produces warnings (unless warnings are treated as errors).
Avoid using the macros in Listing B even though they provide the same functionality as the macros in Listing C. The macros in Listings C, D, and E are modular, reuse code, and are more easily maintained. (See Exercise 2.)
Use the macros PRINT_PX
and PRINT_PXLN
only if the expression to print is placed
inside parentheses and you want the outermost parentheses to be excluded in the
heading.
Based on the discussion in Section 3, does the following chaining of macro invocations compile successfully? If yes, what does the program print? If the code does not compile, illustrate the reason with your own code segment that fully expands the macros in the statement shown. (Imagine you are the pre-processor.)
PRINT("hello") << PRINT(" world");
The macros in Listings B, C, D, and E use std::cout
as the default output stream,
but in some programs, a different stream such as std::cerr
might be better. Modify
each of the programs linked in Listings B, C, D, E to define a single symbol which
stands for the default stream to use and then use the new symbol in the remainder of
the program. Assume the programmer edits the definition of the symbol to set the
default output stream. In all programs, do not alter the main
function in any
way.
With all four programs changed as required, which of the three listings was “easier” to change (less effort to change and less error prone)? Why?
Note: The length of this question notwithstanding, it is relatively simple to the make the required program changes and draw the conclusion asked.
The modularized PRINT_XLN
macro in Listing C does not follow the same
pattern as the other three new-line inserting macros: each of the other three macros
invokes its non-LN
counterpart and then inserts a new line:
In the code linked in Listing C, change the PRINT_XLN
macro to reuse its non-LN
counterpart. Clearly state the differences in the program’s output after the change
and explain the reason for the differences.
Re-write the PRINT_XLN
macro such that the revised macro is still modularized
and it produces the same correct result as the original. (This exercise is trivial.)
Is it possible to write the variadic macro PRINT(x,...)
in Listing E
such that it does not call the function ostream
or any other function? That is,
is there an expression (that does not call some function) which can be used in the
replacement list of the macro to choose the output stream bases on __VA_ARGS__
? If
yes, what is that expression? If no such expression exists, why not? In either case,
show a program to support your position. (Simply modify the program linked in Listing
D.)
Note: The expression sizeof(#__VA_ARGS__)
returns a value greater than 1
if
__VA_ARGS__
is not empty. (By the way, what is the expression’s value if
__VA_ARGS__
is empty, and why that particular value?)
Function ostream
in Listing E requires its argument to be a reference
to a std::ostream
object, but the macros permit any value to be passed as argument
to the variadic part of the macro. This is not really an issue because the compiler
flags an error (try it). However, the error message can be long and somewhat tedious
to process:
What change can be made to the macro definitions or the function ostream
(or
something related to function ostream
) such that the compiler generates a
specific error message you choose? Alter the program linked in Listing E to make
the changes you propose, but do not change the main
function in any way.
After making the changes, do you recommend keeping the changes you made, or would you rather just use Listing E as it is?
Modify the program linked in Listing D to replace as many macros as
possible with function templates. Do not make any changes to the main
function
except to match the names of the new functions developed. Then answer the following
questions:
Are there any macros that cannot be replaced with templates? Why?
With the function templates in place, state the exact number of template
instantiations caused by the code in main
.
Having made the code changes and counted the number of template instantiations, which approach do you recommend: using function templates as much as possible instead of macros, or using only macros? Justify your position in detail. Include a cogent note on the ease of use (reuse) of the solution each approach produces.
Why does function trim_print
in Listing D print the string instead of
returning the input string after removing the first and last characters and let the
calling macro perform printing?
Could the macros PRINT_PX
and PRINT_PXLN
in Listing D themselves
print the expression text without calling trim_print
or another similar function?
If yes, rewrite the macros. If no, describe the reasons.
Why do the macros PRINT_PX
and PRINT_PXLN
in Listing E call function
ostream
to determine the output stream even though function trim_print
is able to
use std::cout
as the output stream by default?
Why does the macro PRINT_PXLN
not reuse PRINT_PX
or another macro? If you
believe reuse is possible, revise the macros in the program linked in Listing E. Do
not change main
function in anyway. Verify that the revised program produces the
same result as the original program.
Ask questions, give feedback, and discuss this post on Twitter+. The Twitter link is specific to this post. We greatly appreciate all discussion on the post being only at the post-specific tweet.
Submit solutions by DM on Twitter (only by DM, please) so as to avoid spoilers. Please provide Compiler Explorer links to code. We prefer textual answers in the form of GitHub gists, files in a repo, or other form where we can just follow a link and open the content in a browser.