Ouch. Modern C++ has alternatives to C macros. Is it an old code base, or is it just written in the C++98 style?
Today people will typically use constexpr instead of #define. While macros can possibly do some funky things constexpr can't, you'd be hard pressed to find those things. (C++ supports constexpr if, constexpr functions, constexpr lambdas, math, and so on.)
If you have the time and effort, it may be worthwhile to slowly start modernizing the code base, bit by bit.
The advantage of modern day C++ is it catches errors at compile time that older versions did not and would crash while the software was running. You might improve the stability of the code base if you help out.
C Macros are lacking because they are very primitive, e.g. they have not type system. They are also hardly turing complete. Its extremely hard to write a meaningful algorithm in them. IMHO the real macros of the C++ language are the templates and constexpr, althou they are limited in other ways. E.g. its hard to extend the syntax using them or do certain things like making the calling function return. They grow ever more powerful, with their own type system (concepts) and things like std::embed and static refection so they finally feel like a real language, alsbei a clumsy, pure functional language that feels alien a C++ programmer without exposure to haskell.
Rust macros are actually meant to feel like Rust, not some ad-hoc bolted on language.
I think this may be why I'm having a hard time appreciating them. Probably half the macros I see could just be a function call. The majority of those that don't are hiding a conditional return or goto, which I find to be a net negative.
I'll probably have to use a language with good macros before I can appreciate them.
Yes, that particular breed of C macros would likely manifest in Rust as people just defining a new function. In Rust, you tend to see macros in places where "just make a new function" doesn't suffice for whatever reason; for example, maybe you need to define a dozen different structs that only differ by the type of one field, so instead of actually defining the struct a dozen times, you could just define the struct inside the macro and then `define_my_struct!(u8); define_my_struct!(u16);` and so on.
You also can't use Rust macros to "redefine" other unrelated pieces of code, so that's one less thing to worry about.
When I say hygienic, I mean that when a macro uses a variable that isn't in the parameters or isn't static, it will be a compile error as it cannot know the scope. Any variables defined inside are scoped. Only parameters parses in can be referenced from outside the scope of the macro
They're also notated different to regular functions and they can't appear anywhere so it's obvious when you see a macro that it will expand into some code that won't break any of the other code in your function.
Of course people can write really terrible macros but typically macros serve a single very simple task and should be well documented and in my experience they often are.
As for procedural macros. They are just ast in, ast out functions, but written as a library in regular rust rather than some language thrown on top. That makes it easy to reason about the code and make it safe. If you write them well, they can be very good at reporting errors in usage to users.
The std lib also provides some very straightforward yet useful macros to act as inspiration. String formatting is an inline macro. vec is a macro to quickly define Vectors. Derive debug, partial equality, default values are all procedural macros and they are very straightforward yet tedious tasks to do on your own all the time.
Given the macros people publish, I would say they've done a good job at securing them as a useful feature. I've not seen many instances in actual code bases of messy macros. For examples of great macros, see serde[0], clap[1], inline-python[2]
[0]: https://github.com/serde-rs/serde [1]: https://github.com/clap-rs/clap [2]: https://docs.rs/inline-python/0.5.3/inline_python/
However, there is one more thing a function can't do: borrowing arguments. Formatting never moves arguments because the format_args! macro generates references to them, which it then creates std::fmt::Argument out of.
I use C++ at work, which admittedly isn't the language I use most, and macros are used quite a bit in the code base. I find they just make the code harder to read, reason about, debug, and sometimes even write. I don't see them really living up to their claimed value.
Is there something different about Rust's macros that make them better?