sigcpp

Special Interest Group on C++

10 Jul 2020

Variadic print macros

Sean Murthy+

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.

1.   Use cases

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:

  1. Print the value of an expression (std::strlen(z)).
  2. Print the value of an expression as heading (typename of a:) and then value of another expression (typeid(a).name()).
  3. Print the text of an expression (sv1.data() != NULL) as heading and then print the value of that expression. This is the most common use case.
  4. Print the value of an expression as heading (duration:), then the value of another expression (elapsed), and then the value of a “tail” expression (s).
Listing A: use cases for print macros
Code
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";
Output
5
typename of a: A10_i
s.data() != NULL: true
duration: 0.0009001s

2.   Initial solution

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.


Listing B: initial print macros (run this code+)
#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');
}

3.   Solution details

Here is a brief note on each of the four macros in Listing B:

  1. PRINTLN(x) std::cout << (x) << '\n': This macro simply prints the value of expression x.

  2. 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.

  3. 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.

  4. 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:

4.   Modularizing the macros

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:


Listing C: modularized print macros (run this code+)
#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'

5.   Handling commas in expression text

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.


Listing D: handling commas in expression text (run this code+)
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
}

6.   Optionally setting the output stream

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:

Listing E shows the use of variadic macros to optionally set the output stream. These macros are enabled by:


Listing E: variadic print macros (run this code+)
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
}

7.   Summary

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:

8.   Exercises

  1. 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");
    
  2. 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.

  3. 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:

    1. 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.

    2. 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.)

  4. 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?)

  5. 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:

    1. 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.

    2. After making the changes, do you recommend keeping the changes you made, or would you rather just use Listing E as it is?

  6. 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:

    1. Are there any macros that cannot be replaced with templates? Why?

    2. With the function templates in place, state the exact number of template instantiations caused by the code in main.

    3. 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.

  7. 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?

  8. 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.

  9. 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?

  10. 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.

Discussion

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.