This is probably not the whole picture, and I have a very Rust-centric view of this, but I'll take a stab at it.
The correct analogue for Lisp macros is not C++ templates, but the C preprocessor itself. Specifically, a Lisp macro gets to take a particular section of code and change it as it wishes, with everything already conveniently tokenized for the programmer's convenience. Imagine if you could just write your own C preprocessor as part of your program and have the compiler automatically execute it on specific program areas that want your preprocessing.
Rust macros work similarly to this, the main difference being your syntax needs to be tokenizable as Rust instead of Lisp. But they're also rather powerful. So, for example, in Rust you only have one object system which has structs, traits, and very limited higher-kindedness[0]. But there's plenty of other object systems Rust would like to interop with: Objective-C, Swift, COM, and C++ among others.
The canonical way of doing this in Rust is to write a macro[1] that takes your class definition and converts it into a series of structs, traits, and/or function pointers that suitably interop with the foreign code. Code outside the macro then can reference the class created by the system.
If you don't have macros, your other options are:
- Metaclasses, which are the canonical way in Python of doing foreign object interfaces, though with an added wrinkle: multiple inheritance from classes of different metaclasses requires writing a combined metaclass that does both. In macros you usually just can't mix them like that, though I doubt you'd need to define a single class accessible from, say, both Objective-C and Windows COM.
- Write your own damned preprocessor. This is what Qt did with MOC (metaobject compiler) to get signals and slots[2]. If C++ had macros, Trolltech probably would have written MOC as a macro instead of a separate build step with a separate C++ tokenizer.
[0] A concept which I will not be explaining in this post, but it has to do with things like generic associated types which were needed for lifetime bounds on async traits
[1] Usually a "procedural macro", which is different from the pattern-matching macros Rust usually teaches in ways that don't matter here
The correct analogue for Lisp macros is not C++ templates, but the C preprocessor itself. Specifically, a Lisp macro gets to take a particular section of code and change it as it wishes, with everything already conveniently tokenized for the programmer's convenience. Imagine if you could just write your own C preprocessor as part of your program and have the compiler automatically execute it on specific program areas that want your preprocessing.
Rust macros work similarly to this, the main difference being your syntax needs to be tokenizable as Rust instead of Lisp. But they're also rather powerful. So, for example, in Rust you only have one object system which has structs, traits, and very limited higher-kindedness[0]. But there's plenty of other object systems Rust would like to interop with: Objective-C, Swift, COM, and C++ among others.
The canonical way of doing this in Rust is to write a macro[1] that takes your class definition and converts it into a series of structs, traits, and/or function pointers that suitably interop with the foreign code. Code outside the macro then can reference the class created by the system.
If you don't have macros, your other options are:
- Metaclasses, which are the canonical way in Python of doing foreign object interfaces, though with an added wrinkle: multiple inheritance from classes of different metaclasses requires writing a combined metaclass that does both. In macros you usually just can't mix them like that, though I doubt you'd need to define a single class accessible from, say, both Objective-C and Windows COM.
- Write your own damned preprocessor. This is what Qt did with MOC (metaobject compiler) to get signals and slots[2]. If C++ had macros, Trolltech probably would have written MOC as a macro instead of a separate build step with a separate C++ tokenizer.
[0] A concept which I will not be explaining in this post, but it has to do with things like generic associated types which were needed for lifetime bounds on async traits
[1] Usually a "procedural macro", which is different from the pattern-matching macros Rust usually teaches in ways that don't matter here
[2] https://en.wikipedia.org/wiki/Signals_and_slots