7 ms·
So it looks like people repeat how C++ templates are bad, and Rust macros are good. Can someone tell me what's the real difference? I'm not sure I get it, but I
by pleasecalllater 9y ago
So it looks like people repeat how C++ templates are bad, and Rust macros are good. Can someone tell me what's the real difference? I'm not sure I get it, but I feel like both are good or both are bad.
- jackninja1 9y agoThose are like different concepts. While Rust macros work on the AST and take AST nodes as arguments C++ templates are a whole meta language that also allows for code execution. I would say that Rust macros mainly do code generation while C++ templates also allow for computation.
- steveklabnik 9y agoC++ templates are used for two different features in Rust: generics, and a more broad "metaprogramming." The other threads talk a bit about general metaprogramming, so let's talk about the other bit. On the generics side, here's an illustrative example, taken from http://en.cppreference.com/w/cpp/language/function_template http://en.cppreference.com/w/cpp/language/function_template #include <iostream> template<typename T> void f(T s) { std::cout << s << '\n'; } int main() { f(7); } Here, the function f will take any type T, and print it out. in this case, 7 is an int, so T is int. In Rust, here's the same function: fn f<T>(s: T) { println!("{}", s); } fn main() { f(7); } here, 7 is an i32, so T is i32. But, we get a compilation error in Rust: error[E0277]: the trait bound `T: std::fmt::Display` is not satisfied --> src/main.rs:2:22 | 2 | println!("{}", s); | ^ `T` cannot be formatted with the default formatter; try using `:?` instead if you are using a format string | = help: the trait `std::fmt::Display` is not implemented for `T` = help: consider adding a `where T: std::fmt::Display` bound = note: required by `std::fmt::Display::fmt` error: aborting due to previous error This illuminates a core difference between Rust's generics and C++'s templates: in the C++ case, the code is typechecked after the code is expanded. So it works. However, we don't even need that `main` to cause a compilation error in Rust; it only checks the body of the function. In this case, Rust says "Hey, you accept any T, but you try to print it out. What if a type isn't printable? That wouldn't work!" The fix is: use std::fmt::Display; fn f<T: Display>(s: T) // or in near-future Rust, a slightly nicer syntax, still needs the 'use' line too: fn f(s: impl Display) This changes the signature to say "we take any type T that implements the Display trait." Now we know that the body of the function works properly. There are advantages and disadvantages to both approaches. In some senses, the tradeoffs are the same as any duck typed vs not situation: the C++ approach is far more flexible, but the Rust approach has stronger guarantees. We also chose the style we did because we prefer the nicer errors; C++ template errors are notoriously complex, though many say they get used to how to read them over time. Additionally, neither languages' story is complete here. C++20 has a new feature called "Concepts" that gives you some degree of similar features here, but it's complex and I haven't read the version that was merged into the draft of C++20, so maybe someone else can elaborate on it. Rust is also adding a 'const fn' feature that gives you access to similar tricks to constexpr in C++, as well as more features for our bounded generics.
- pjmlp 9y agoYour example in C++17, #include <iostream> #include <string> #include <type_traits> class Display { virtual std::string fmt() = 0; }; template<typename T> void f(T s) { static_assert(std::is_base_of<Display, T>::value, "Error: T must implement Display interface"); std::cout << s << '\n'; } int main() { f(7); } Gives the following error with clang, <source>:12:7: error: static_assert failed "Error: T must implement Display interface" static_assert(std::is_base_of<Display, T>::value, "Error: T must implement Display interface"); ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ <source>:19:7: note: in instantiation of function template specialization 'f<int>' requested here f(7); ^ 1 error generated. Compiler returned: 1 Of course the downside is having the library author spending extra effort to write those checks.
- steveklabnik 9y agoTotally! There's a lot of code out there that doesn't do this, even if it could. That's also a lot more code, so I imagine people do it less often as well. Beyond that as well, you won't get an error if you don't use f(7), that is, if your main is empty, which would affect library authors.
- ljrmorgan 9y agoThis is not really the same though, because the Rust version compiles after the bounds have been added: use std::fmt::Display; fn f<T: Display>(s: T) { println!("{}", s); } fn main() { f(7); } whereas there would be no way to get `f(7)` to compile in your C++ version since you can't make `int` inherit from `Display`. _A_ downside is that the library author has to add these sorts of checks, but for me a bigger downside is that anyone who then wants to use the library has to inherit from the library's types.
- Rusky 9y agoConcepts (lite, the version that is still around) are "one-way." They constrain what types you can use as template arguments, but they do not constrain what templates can do. Which means the templates themselves can still impose additional requirements based only on their internals, leading to the same bad error messages for users and lack of checking for library authors that we have without concepts.
- taeric 9y agoBoth make debugging harder. Rust, though, keeps a lot more safety in the whole process. The resulting code is syntactically what the macro author meant, by the types. And, nobody really steps through code anymore. So a lot of the difficulty argument is moot.
- fb03 9y ago> And, nobody really steps through code anymore. I don't follow. What other technique is in, compared to code stepping? Sometimes I put a breakpoint in and step thru code to help me figure if something is working properly or maybe live-inspect the return value of something that comes from the outside world. Maybe I'm missing some new fundamental technique to dev/debug? Thanks in advance. (genuine question)
- steveklabnik 9y ago"printf debugging" vs "debugging" is an eternal struggle. Many people do both, in both C++ and Rust. There are many, many essays written about the pros and cons of both approaches. Rust as a project is investing heavily in working towards excellent debugger support, because people do use it.
- nalllar 9y agoDebugging in CLion with gdb and the intellij-rust plugin is already quite pleasant. It even works remotely attached to bare metal embedded projects on stm32 microcontrollers! Unfortunately debugging isn't available with intellij-rust in the free IDEA community edition.
- taeric 9y agoHow well does it work with heavy use of macros? Specifically, what does it mean to "step into" a heavily macro based section? I'd be very interested in seeing examples.
- taeric 9y ago
- pcwalton 9y agoC++ templates are intertwined with parsing and typechecking, while Rust has two separate features that cover the same ground: macros, which are purely parse time, and generics, which are purely run at typechecking time. This has advantages and disadvantages. The advantage of C++'s model is that it has one feature instead of two, and this allows for features like SFINAE that Rust has no analogue to. The advantages of Rust's model are that (1) the implementation is a lot simpler; (2) you can typecheck generics before they're expanded, which is a big improvement in ergonomics; (3) generics cover 90% of the use cases, so you rarely have to reach for macros.
- justinpombrio 9y agoC++ templates also aren't general purpose macros. For example, in Rust you can write a macro that takes an expression `e` and replaces it with `e + 1`. However, I believe you can't do that with C++ templates. C++ template parameters can be types or primitive values, but not expressions. And the body of a template has to be some sort of declaration, e.g. a function definition or struct definition, but not an expression or statement.
- eddyb 9y agoRight, you can write a function that returns its argument plus one, but it couldn't e.g. evaluate `e` more than once (which both C and Rust macros can).
- lucozade 9y agoAs a, slightly facetious, addition. I think Rust macros are better than either the C preprocessor or C++ templates because they're so bloody confusing. They require someone with a great deal of energy, knowledge and will to win in order to tease anything useful out of them. This, I submit, is a feature. It dissuades the programmer who believes they are better than they are from being too clever by half. There are worse things in the programming sphere than crap meta-programming but it does have its own circle in hell. I appreciate that the Rust team are working hard to spoil my happy idyll but happy I remain for now.
- kibwen 9y agoI tend to agree with the argument that Rust macros are deliberately ugly so that people will only reach for them as a last resort (learning the hard-won lesson from Lisp, as it were), but there's no way that Rust macros are any more confusing than the C preprocessor or C++ templates. Have you ever tried to write a C macro that actually expands to what you want it to, in all possible contexts? And the less said about the horrors of C++ templates, the better. No, the fact that Rust macros have severe (deliberate) limitations is what makes them tractable; there's no shenangians comparable to `#define true false`.
- lucozade 9y agoTh Rust macro system is difficult to use. So it doesn't get used a lot. But when it does it tends to be used well. The C preprocessor and C++ templates are not difficult to use. But they're both difficult to use well. So they get used. A lot. Badly.
- devit 9y agoC++ templates are bad because they are untyped generics (e.g. C++ doesn't check that the key type for an hashmap is hashable, and instead you get a bunch of errors that there is no "hash" overload accepting certain arguments when you try to call a method on the hashmap). C++ tried to add a type system for templates with "concepts" but it was scrapped. Rust instead has properly typed generics instead using the trait system, resulting in proper static checking and proper error messages (although without higher kinded types and const generics currently, that C++ instead supports). Rust also has a powerful macro system, which is unrelated to templates, although you can use them to have kinds of abstractions that the language doesn't support directly (like higher kinded types, const generic or abstracting over mutability), at the cost of having to explicitly perform instantiation, not having inference and having a higher cognitive burder.
- steveklabnik 9y ago> C++ tried to add a type system for templates with "concepts" but it was scrapped. concepts was merged into the C++2020 draft recently, so not entirely scrapped! I forget if it was full-blown concepts or "concepts lite" though.
- pjmlp 9y agoConcepts are on track for C++20 and they are already available on gcc to play with. Besides that, it is already possible to use typed templates in C++, at least with any C++14 compiler and even better on C++17, even if it requires a bit more of effort for the implementer of the generic code. Basically by making use of type traits, static asserts, constexpr if and enable_if. Yes, it isn't as clean as Rust but as shown by Andrei Alexandrescu with D, it opens the door for very powerful designs.
- ogoffart 9y agoBut the C++20 concepts is only concept lite. It does not check the implementation of the template function that it only uses operations available for the conceps. suppose you have a concept Hashable that is only enabled for type with a hash() function. But you can still in the code of your HashMap do a comparison with the < operator, which the Hashable concept don't ensure. In that case you woud only have an error at instentiation type for Hashable types that don't have '<' In Rust, the compiler make sure that your generic function only use operations that exits in the traits declared in the function signature.