The C and C++ preprocessors are like the Windows command prompt batch language: They implement a very simple language that people still use for some reason, usually by pushing it way beyond its original design boundaries.
The preprocessors don’t really understand the C or C++ language. It does, fortunately, use the same arithmetic operators that the C and C++ language uses, but its understanding of them is limited to integers.
The treatment of commas in preprocessor arguments is very simple: Commas separate macro arguments. The only way to protect a comma is to enclose it in matched parentheses.
#define M(cond, action) if (cond) action // horrible macro M(function(2, 3), return) // okay
Note that the less-than and greater-than signs do not protect commas. This can create confusion if you want to pass a template instantiation as a macro parameter.
M(std::is_same_v<T, U>, return); // doesn't compile
The preprocessor isn’t smart enough to realize that what you passed was intended to be a template instantiation. After all, maybe you wanted this:
M(value < 0 || height > 1000, return out_of_range);
In this case, the less-than and greater-than signs are intended to be the comparison operators. But from the preprocessor’s point of view, the angle brackets in
std::is_same_v< T, U >
and
value < 0 || height > 1000
are basically the same thing. It has no way of knowing that the first case of matched angle brackets is a template instantiation, whereas the second is just a pair of comparison operators. Not even the compiler knows, because we are still preprocessing. Compilation hasn’t happened yet.
The solution is to insert seemingly-spurious parentheses around the macro argument.
M((std::is_same_v<T, U>), return);
template code in a preprocessor macro sounds like asking for a confusing really long error message if your macro generates some bad code.
I'm now wondering what happens to the parentheses if the macro passes that argument to another macro. I imagine nothing happens to them. Also wondering about what expressions might not be valid with the parentheses but valid without. My intuition says parentheses around a type name is going to lead to a bad...
A good trick to avoid the need of parenthesis when macro arguments contain commas is to use __VA_ARGS__ as macro argument.
For example we could write the above macro as
#define M(action, …) if (__VA_ARGS__) action // still horrible macro
and then call as
M(return, std::is_same_v<T, U>);
Until you want to do
M(return std::is_same_v<T, U>, std::is_same_v<X, Y>)
!
Angle brackets have been used for “less than” and “greater than” for a very long time. Maybe using them to signify template instantiation wasn’t a very good design decision.
Humans and machines can both understand template brackets just fine. I’d rather lay the blame on the unsafe and rather unintelligent C preprocessor than on the template syntax.
In order to make the preprocessor more intelligent, it would have to run on concert with the compiler rather than as an earlier translation phase. Whether
M(a<b,c>-1)
is a macro invocation with one parameter or two depends on whethera
names a template. Sometimes the best solution for a system that isn’t smart enough is to make it stupider and therefore more predictable.Not to mention angle brackets are used in other languages to signify generics, so it makes sense to be consistent.