6 ms·
C++ template macroprogramming versus Lisp macros
- liontwist 2y agoLisp macros can take arbitrary parameters and are written in lisp. C++ macros can only take types and numbers (until variadic), and writing any code to operate on those inputs is challenging.
- dataangel 2y agoC++ templates can also take other C++ templates (the template itself, not just an instantiation), enum values and I think in C++23 maybe even structs but I need to check
- jandrewrogers 2y agoC++20 added the ability to take class instances as non-type parameters but the rules around the types of classes that can be used in this way are pretty restrictive (for good reason).
- spacechild1 2y agoThe post is actually about template metaprogramming. 'template macroprogramming' isn't really a thing.
- knome 2y agohttps://github.com/knome/metabrainfuck/blob/master/bf.cpp https://github.com/knome/metabrainfuck/blob/master/bf.cpp It's not too bad :)
- binary132 2y agoYes, the purpose of C++ templates (not macros, macros are C preprocessor) is to implement generic classes and functions, not to implement arbitrary computation, and (iirc) it was only discovered after their publication that they are in fact Turing-complete.
- scott_s 2y agoAgreed with the technical content and conclusion. However, I think it is worth pointing out that since C++11, it has had a mechanism to specify (maybe) compile-time computations that are written in plain C++: constexpr, https://en.cppreference.com/w/cpp/language/constexpr https://en.cppreference.com/w/cpp/language/constexpr (My parenthetical "maybe" is that I don't think compilers have to compute constexpr expressions at compile time. The compiler will be forced to when such expressions are used in contexts that require values at compile time. But I think it would be permissible for a compile to defer computation of a constexpr to runtime if the value isn't needed until runtime.)
- pjmlp 2y agoBesides consteval and constinit, you can force evaluation with static constexpr.
- spacechild1 2y agoNote that C++20 introduced consteval as a means to enforce compile time computation. See https://en.cppreference.com/w/cpp/language/consteval https://en.cppreference.com/w/cpp/language/consteval
- pjmlp 2y agoFactorial macro example in C++23 metaprogramming, #include <iostream> consteval long factorial (int n) { if (n == 0) return 1; return n * factorial(n - 1); } int main() { std::cout << factorial(7) << std::endl; } Exercise for the reader if using VC++ or clang/ninja, use import std instead. -- https://godbolt.org/z/TWe11hM6j https://godbolt.org/z/TWe11hM6j Nicely put 5040 in ESI register at compile time. Granted, C++ isn't Lisp, but already has quite a room for creativity at compile time, and C++26 might finally have compile time reflection as well.
- _19qg 2y agomacro programming in Lisp would be "programming programs". I don't see it here. This looks like compile-time execution to compute values. If it would be a macro, it could return source code.
- pjmlp 2y agoIn general, a macro produces code at compile time, doesn't matter in which form it ends up in the binary, as long as the observable side effects are the same. Example of a library that generates serialization code, https://github.com/getml/reflect-cpp https://github.com/getml/reflect-cpp As mentioned the ongoing C++26 proposal with produce the desired source code at compile time, thus reducing the amount of code of libraries such that one.
- lispm 2y ago> In general, a macro produces code at compile time The term "macro" is overloaded. In a typical Lisp they are generating source as data, not just constant values. Lisp macros also work in interpreters and they can use arbitrary language features, incl. creating side effects or accessing external data sources.
- emmanueloga_ 2y agoSome people are calling this "multistage programming" [1] which is kind of related but not exactly the same as a macro system. -- 1: https://en.wikipedia.org/wiki/Multi-stage_programming https://en.wikipedia.org/wiki/Multi-stage_programming
- forrestthewoods 2y agoI don’t have any experience with Lisp. But I think C++ templates and Rust macros are both super bad and impoverished compared to what can be done in Jai. https://www.forrestthewoods.com/blog/using-jais-unique-and-powerful-compiler-for-typesafe-units/ https://www.forrestthewoods.com/blog/using-jais-unique-and-p...
- SkiFire13 2y agoThe `#modify` thing looks pretty cool, but I can't help but think how a compiler/ide is supposed to analyze the body of such function and provide suggestions. Rust macros have a similar issue, but it's offsetted by the fact that generics are pretty powerful too and can be fully analyzed by the compiler.
- pjmlp 2y agoExcept Jai is never going to get the use any of them have, so we use what is there.
- forrestthewoods 2y agoThat is not a coherent sentence. I share it not to say “use Jai instead of C++/Rust”. But to instead say “templates and macros suck and it’d be great if Rust or other language copied good ideas into their ecosystem”.
- pjmlp 2y agoFirst we need to be able to use Jai, so that those folks can validate in practice how it works, not watching videos about someone using their creation.
- forrestthewoods 2y agoSomeone has to lead by example. Jai is trying things no one else is doing. No reason that Rust or other more widely available regions can’t lead. Rust certainly led the charge with the borrow checker. Why not do better than macros and templates and generics?
- lispm 2y ago(defmacro factorial (n) (labels ((fact (m) (if (= m 0) 1 (* m (fact (1- m)))))) `,(fact n))) The `, has no use here and can be removed. Here the backquote and the evaluation just returns the computed value. Thus, this is okay: (defmacro factorial (n) (labels ((fact (m) (if (= m 0) 1 (* m (fact (1- m)))))) (fact n))) LABELS defines local recursive functions. The macro returns the result of calling FACT, which is a number and which is a valid form in Common Lisp. A number evaluates to itself. CL-USER > (macroexpand-1 '(factorial 10)) 3628800 T
- qertuiopas 2y ago[flagged]
- James_K 2y agoMacaroni art versus the Mona Lisa.
- thunkingdeep 2y agoCommon misconception of non Lispers that macros are equivalent to compile time programming. You’re not simply moving the evaluation to compile time, you’re moving it upwards outside the program space into a higher dimension of programmability. Not to dog on C++ unfairly, CTE is pretty neat after all. Funnily enough, PGs “On Lisp” has some really neat macros in it that demonstrate capabilities that just can’t be replicated with template based macros, iirc.
- nightowl_games 2y ago"outside the program space into a higher dimension of programmability" I can visualize this metaphor just fine, but I can't tell why it's useful. Can you make this concept more concrete?
- eddd-ddde 2y agoI think an example would be helpful, conditionals: Within the "program dimension" there is just no way to run code conditionally without an if, no matter how much you move left and right, you are constrained. It is only possible by using the "higher dimension".
- jcul 2y agoThis is different from constexpr if in C++? Where one branch will not exist in the compiled code?
- Jtsummers 2y agoLisp macros take zero [0] or more unevaluated forms, does something with them (which may be computing a value, see the factorial example elsewhere in this discussion and the submitted blog), and then returns a new form (which could be that computed value, again see the factorial example) which the Lisp system evaluates eventually. One way to see this would be to do something like: (defmacro print-value (a) (print a) a) (print (print-value 1)) (print (print-value (+ 1 2)) ;; output 1 1 (+ 1 2) 3 https://tio.run/##S87JLC74/18jJTUtNzG5KF@hoCgzr0S3LDGnNFVBI1FTQQMsoABkJWpycUF5GsiqDDU1sYpraCsYKhhpamr@/w8A https://tio.run/##S87JLC74/18jJTUtNzG5KF@hoCgzr0S3LDGnNFVBI1... The value bound to a inside the macro is the unevaluated form (the first and third items printed out). In this case the macro itself is just the identity macro other than its print effect so it returns the original form, which is why we get 1 and 3 printed as well. Here's an example of processing (very primitive) the unevaluated form to produce something new: (defmacro infix (a op b) `(,op ,a ,b)) ;; alternatively: (list op a b) (print (infix 10 + 20)) ;; output 30 Now we can get fancy (this is primitive still, but works): https://tio.run/##fZDBDoMgEETvfsXculh7qP2hImJKSpUgSf17uiqJ1Vr3QMjbmVkWZU3vYqRaNy@pfAfTNmYA6cF5kQFkZaVtD6K3tM@Fb4pU19a/mBvEA4LD5MOdCur1qE2gSLGN8X2Y2QLDw/gk5NpND@v2bEwoI@dNG0DTYgwljz1RWrBEjpv4lm0ax3aJM6qV3Wo@mF8FqELJe/zX7kayLocSLGb/hW@H8fzl4zNx6I7xAw https://tio.run/##fZDBDoMgEETvfsXculh7qP2hImJKSpUgSf17uiqJ1V... (defmacro infix (expr) (labels ((walk (expr) (cond ((listp expr) `(,(second expr) ,(walk (first expr)) ,(walk (third expr)))) (t expr)))) (walk expr))) This generates a valid (prefix notation) lisp form from a more traditional infix form from mathematics. If I were being more clever I wouldn't use the second item in the list (the operation) directly but restrict it to valid arithmetic operators and change how I walk the structure. That would remove the need to force explicit parentheses since I could add in a proper parsing step. This is a very primitive version of what loop and other complex macros do. They take essentially a different language, parse it, and emit a valid Lisp form which is then evaluated. You could use this to get constexpr like behavior but once you do that you run into problems, you can't do this for example: (defmacro foo (a b) (+ a b)) (foo (+ 1 2) (+ 3 4)) ;; error (let ((a 1) (b 2)) (foo a b)) ;; error (foo 1 2) ;; => 3 It only partially works because it only works, when a and b are both numbers. If they're symbols (second case) or other forms (first case) then the macro attempts to compute something that cannot be computed. You can fix the first case by doing: (defmacro foo (a b) (+ (eval a) (eval b)) But that still leaves the second case erroring out. You could do something like what I did with infix which walks the forms and determines if they can be evaluated (no unbound variables) and then evaluate them conditionally, leaving expressions with unbound symbols intact to be processed later. So C++ constexprs are less than Lisp macros, but if you want to use Lisp macros to do the same thing as constexprs you have to do more work. Check out On Lisp and Let Over Lambda for two books that go deep into macros in Common Lisp. [0] I honestly don't know why you'd want to do this, but technically it's valid to do: (defmacro foo () (some form to return)) (macroexpand '(foo)) ;; => (some form to return) I cannot think of a case where this would be useful, but someone else might think of one.
- mgaunard 2y agoDoes this article have a (2002) that I missed or something?
- andyg_blog 2y agoI hate to agree. For an article about metaprogramming in C++, you'd expect expert level C++ code, and not what's presented here. It's bad. And the first example with the list even has a bug.
- mwkaufma 2y agoThe "next" field in the lead code sample is supposed to be a pointer, right?
- rottc0dd 2y agoAnother post comparing C and lisp Macros: http://lists.warhead.org.uk/pipermail/iwe/2005-July/000130.html http://lists.warhead.org.uk/pipermail/iwe/2005-July/000130.h... HN discussion: https://news.ycombinator.com/item?id=31199992 https://news.ycombinator.com/item?id=31199992
- binary132 2y agoIt’s clumsy because C++ templates are an implementation of generic and dependent types, while C++ constexpr and consteval functions are for arbitrary computation. The fact that template metaprogramming _can_ be used for arbitrary computation is actually an unhappy accident of its design that was only proven after its publication.
- anothername12 2y agoPffft, I’d just #. that factorial call and it’d be a normal defun.
- unnah 2y agoC++ templates do have the advantage that they can dispatch on their argument types. Lisp macros are expanded at a purely syntactic level, well before the compiler attempts to do any type deduction, and you cannot provide different macro expansions for different argument types without heroic efforts (which will most likely be specific to a single Lisp compiler). Dispatching on argument types is very useful in expression template based metaprogramming, for example in the Eigen library for linear algebra (https://eigen.tuxfamily.org/ https://eigen.tuxfamily.org/).