Reducing usage of macros in C++ programming

30 Nov 2021 - John Z. Li

The problems with macros are well understood by C and C++ programmers. Basically, the preprocessor operates on the source code independently and before the compiler has a chance to check the code. Macros are basically textual replacement instructions that does not understand the syntax of the host language (C or C++). Macro expansion can also be affected by defining or not defining certain environmental variables or the configuration of the building phase. This poses challenges for certain things like static analysis and debugging to be done in C and C++. But with C, macros are the only thing that can make certain things easier. With C++, we generally should use less macros if there are other ways to achieve what macros can do.

Macros that can be replaced with modern C++ features

Macros that can be replaced with constexpr expressions

Consider the following macros:

#define PI 3.14
#define TWO_THOUSAND  1000*2
#define FILENAME "my_file_with_a_very_long_name.hpp"

This kind of macros can be replaced by constexpr expressions:

constexpr auto Pi = 3.14;
constexpr auto Two_thousnd = 1000 * 2;
constexpr auto Filename = "my_file_with_a_very_long_name";

If these definitions are to be used in multiple translation units, one can also apply the inline keyword to them (After C++17). Sometimes, macros are used to introduce an alias of a function, for example

#define PRINT printf

This kind of aliases can also be replaced with simple constexpr expressions. The above code can be replaced by

constexpr auto Print = printf;

Macros that can be replaced with lambda functions

Consider the following example:

#define CIRCLE_AREA = PI * R * R
//
int main()
{
	double R = 10;
	double area = CIRCLE_AREA;
}

Here, the macro CIRCLE_AREA refers to a variable R that is to be defined before the invoking site of the macro. This kind of macros act like lambda functions in that they “capture” the context where they are invoked. Thus, this kind of macros can be easily replaced by

// assuming Pi has already been defined
int main()
{
	double r = 10;
	auto circle_area = [&r](){return Pi * r * r};
	double area = circle_area;
}

Macros that can be replaced with using declarations or typedefs.

Consider the following example:

#define RECORD vector<int>
#define SIZE_T unsigned int
#define RECORD_PRT RECORD*

This kind of macros can be replaced by so-called type alias:

using Record = vector<int>;
using Size_type = unsigned int;
using Record_ptr = Record *;

In C, one often needs to use macros to achieve generic data structures. The following code is taken from “openbsd/src/sys/sys/queue.h”:

/*
 * Singly-linked List definitions.
 */
#define SLIST_HEAD(name, type)						\
struct name {								\
	struct type *slh_first;	/* first element */			\
}

#define	SLIST_HEAD_INITIALIZER(head)					\
	{ NULL }

#define SLIST_ENTRY(type)						\
struct {								\
	struct type *sle_next;	/* next element */			\
}

In C++, generic programming is achieved by templates. To give a new name to a template, we can use the so-called alias template like below:

template<typename T>
using List = std::list<T>;

typedef can also achieve the same purpose, but it has to be wrapped in a struct template, thus more verbose:

template<typename T>
struct List{
	typedef std::list<T> type;
};

Function-like Macros that can be replaced by function templates

Consider the following typical usage of macros:

#define MIN(A, B) ((A) < (B) ? (A) : (B))

Since we don’t know anything about A and B, like whether they are values or references, whether they are const qualified, whether they are lvalues or rvalues, we have to use perfect forwarding along with decltype on the return value in function templates to mimic its behavior:

template <typename T1, typename T2>
inline auto MIN(T1&& A, T2&& B)
-> decltype(((A) < (B) ? (A) : (B)))
{
return ((A) < (B) ? (A) : (B));
}

The inline keyword here can be omitted if we are only going to implicitly instantiate the function template. In this case, it is implicitly declared as inline.

Similarly, variadic macros can be replaced by variadic templates. Consider the following macro definition:

// use compiler extension, so that when __VAR_ARGS is empty, there is no trailing comma
#define debug(format, ...)  printf(format, ## __VA_ARGS__)
// or more portalbe way
#define debug(format, ...) printf(format, __VAR_OPT(,) __VAR_ARGS__)

The above code can be replaced by the following variadic function template:

template <typename... Args>
inline void debug_str(const char * fmt_str, Args&&... args) {
    printf(rt_fmt_str, std::forward(args...));
	return;
}

Note: don’t use this code in real projects. Use std::format introduced in C++20, or fmt from which std::format is modeled.

If a function-like macro calls an function object or a lambda function which is only accessible from the context where the macro is invoked, it should be replaced by a lambda function as discussed in a previous section. The lambda function itself might needs to be generic or even be templated, Lambda templates are a feature introduced in C++20 while generic lambda is a C++14 feature. In case the macro is variadic, its replacements need also to be variadic.

Get information of source code information

The preprocessor recognizes the following macros (note __func__ is not a macro):

Macros that can be partially replaced by C++ features

Conditional compilation

C++17 introduced if constexpr, which can replace some use cases of conditional compilation using #if and friends. An example of this is like below:

if constexpr (DEBUG_LEVEL_INFO)
    {
        log(\* do some logging if only debug level is set to Info*\);
    }

This kind of stuff is traditionally done by macros, for example from folly/logging

disabled log statements should boil down to a single conditional check. Arguments for the log message should not be evaluated if the log message is not enabled. Unfortunately, this generally means that logging must be done using preprocessor macros.

But if constexpr can not replace or use cases of conditional compilation, because even if a branch in a if constexpr statement is going to be ‘optimized’ away in compile-time because the condition is an constexpr equals false, the code inside that branch still needs to be valid code. This means, if constexpr can not be used to deal with platform specific things, like calling a platform specific function only when built against that platform.

Increasing code readability and easing maintainability using macros.

Generally, macros tend to make code less readable, and can cause maintainability headache if over used. But sometimes, using macros can actually make code more readable and easier to maintain.

  1. An example of this is X macros. The idea is to use macros to generate boilerplate code, which, if written by hand, can be error-prone or boring.

  2. Another example is like above
     #define AUTO_COUT(x) {\
      boost::io::ios_all_saver ias( cout );\
      x;\
      }while(0)
    

    This code is used to restore ostream format settings back to the original state, so that things like std::hex stops in effect after it is being called.

  3. Yet another example is when you want to specialize std::greater<> and friends on types that don’t have comparison operators defined like below: ```cpp #define MAKE_COMP(class_name, comparison, field, symbol)
    template <> struct std::comparison { \ bool operator()(const class_name &lhs, const class_name &rhs) { \ return lhs.field symbol rhs.field; \ } \ };

struct person { unsigned int age; std::string name; };

MAKE_COMP(person, greater, age, >) MAKE_COMP(person, less, age, <) MAKE_COMP(person, equal_to, age, ==) MAKE_COMP(person, not_equal_to, age, !=)

There will be quite some code to write if we are going to write these code manually.
Maybe after static reflection is introduced in C++, such repetitive code can be generated without macros.
4. The `FWD` macro and `LIFT` macro
I won't repeat what is in this good [blog post](https://blog.tartanllama.xyz/passing-overload-sets/).
The point here is that using macros can sometimes make your code more readable.
This kind of things can be done without macros at the cost of writing more boilerplate code.
Maybe new features added into the language in the future will reduce macro usage.

## Things that can only be done via macros (up till now)
### Language feature test.
There are a whole lot of [feature test macros now in C++](https://en.cppreference.com/w/User:D41D8CD98F/feature_testing_macros).
Their sole purpose is to determine whether a certain language feature is supported by the compiler.
Currently, this can only be achieved by macros.
### Stringification
Stringification is the ability to treat any sequence of characters as a compile-time `const char` array, that is C-style string.
For example:
```cpp
#define STR(x) #x

auto enum_name = STR(Color::Green);
auto class_name = STR(my_namespace::my_class);
auto type_name = STR(vector<bool>);

It is equivalent to:

const char* enum_name = "Color::Green";
const char* class_name = "my_namespace::my_class";
const char* typename = STR(vector<bool>);

This itself does not seem very impressive. But it can be a powerful tool when it is combined with other macro techniques like X-macros. For example, you can define the following enum_to_string function like below so that it can return a string that contains the enum value’s corresponding name and its value:

#include <iostream>
#include <string>

#define COLOR \
  X(Green, 1) \
  X(Blue, 2)  \
  X(Red, 4)

#define X(x, ...) x __VA_OPT__(= __VA_ARGS__),
enum class Color { COLOR };
#undef X

std::string enum_to_string(Color c) {
  std::string enum_name, enum_value, enum_str;
  switch (c) {
  #define X(x, ...) \
    case Color::x: \
      enum_name = "Color::" #x;   \
      enum_value = std::to_string(static_cast<int>(c)); \
      break;
    COLOR
  #undef X
  }
  enum_str = enum_name + "=" + enum_value;
  return enum_str;
}

The beauty of this is that whenever the definition of the macro is changed, like adding a new element to the enum, one only needs to change the definition of COLOR. Currently, this trick can not work without macros. This will change after static reflection is added to the language.

Batch generating identifier names

In C and C++, macros can be concatenated using the ## operator. This is extremely useful when we need to generate many different identifier names. An example is PYBIND11_EMBEDDED_MODULE and friends in pybind11. Basically, to call C++ library from Python, we need to create functions like extern "C" PyObject *pybind11_init_impl_##name(); with different names for different libraries. It will be very inconvenient if the users are asked to write these function names by hand. C++ test frameworks also heavily rely on this trick to automatically generate many identifier names that one would not bother to give names manually.