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.
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.
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.
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.
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.
I thought it was possible to do something like this? (disclaimer: I don't use c++)
class Hashable { abstract char *hash() = 0; } template<typename T: Hashable, typename U> class Hashmap { vector<U> buckets; // etc. }
Since you get some kind of compile-time diagnostic, then effectively, the type is checked. Just, within static time, not as early as you'd like, with not as a relevant a diagnostic.
ISO C++ doesn't dictate the wording of diagnostics; a C++ implementation could work backward from that error, or do whatever else, in order to phrase the diagnostic in terms of a problem in the template.
C++ templates being checked only for instantiated types makes them flexible. E.g. a template that accesses type::foo or instance.foo will work with anything that has a member named foo. That template argument doesn't have to have a declared relationship to any type which has foo, which would be an annoying restriction for the users.
Types that aren't instantiated don't exist. They don't correspond to anything in the executable image. Worrying about them is like a recording studio fussing over how much reverb to add to a one-handed clap.
It's fine to not worry about types that don't get instantiated if you are the only person ever instantiating a template, but that's obviously not always the case. If my template doesn't typecheck for a type that someone else wrote, I don't find out about it until they send me a bug report. With typechecking before instantiating, I find out about it as soon as I write it.
> A type error that doesn't execute doesn't really exist either.
Nope; that depends on what you mean by "doesn't execute".
Provably doesn't execute? As in: it is removable, dead code?
Or: doesn't execute because it's not covered by a test case? So that it could execute if a suitable input is found?
A template-generated type that is not instantiated doesn't execute because it doesn't exist. It's not in the image. There is no possible input to the program which can exercise it.
The code is basically not written. There is no need to diagnose bugs in code that isn't written.
Making users derive template arguments from a specific type is just stupidly "blubby". That C++ has typeless template parameters is one of the few sane things about the way it supports generic programming.
> With typechecking before instantiating, I find out about it as soon as I write it.
With type checking, that same user who would have run into a problem with the "typeless template" will simply find your template useless, and roll their own. In other words, same situation as before. The type error will still be there.
If you have a template that is intennded with foo-like arguments, and the user uses them with a bar-like object, that's going to be diagnosed one way or another.
If the generic arguments are weak, it will show up as some constraint violation when the template-generate code uses the the bar-like thing as if it were foo-like.
If the generic arguments are strong, the user instead gets an error that the bar-like thing is not a suitable argument to the template, which expects a foo-like.
It's the same either way; it's all static checking at the same static time and it doesn't change the circumstances under which errors occur. It just changes the amount of discipline needed to use the generic, and the accuracy/granularity of the diagnostic ("this thing is not a foo" versus "this thing here coming from a template parameter can't be used in this way in this piece of template generated code").
It's easier for the user to adjust the bar object to make it sufficiently foo-like for a given template, than to make it completely conform to the foo type.
Unless you have a bug in your template that causes it to have a type error for a type that you didn't intend for it to have a type error for.
You might intend (without actually testing) that the template works with some bar also, not related to foo; the user tries it and finds otherwise, due to a bug.
With the typed generic, you just declare that it's for foo, and it refuses to work with bar (whether or not it has a bug which would actually prevent that in the absence of the declaration).
All the declaring, checking, instantiation, and the rest of it are all happening in the same time: static time. There is no difference like with static versus dynamic. Static versus dynamic is due to different times: what is done before running the program, versus during.
There is no verification difference between declaring that a template parameter may be of types foo | bar, and writing a typeless template which a test module then instantiates for foo and bar. Both are declarative mechanisms at work, played out statically. Just one way is more open-ended; it's not "sealed" against new instantiations.
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.
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.
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
#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.
#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.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.
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.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.
And, nobody really steps through code anymore. So a lot of the difficulty argument is moot.
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)
Rust as a project is investing heavily in working towards excellent debugger support, because people do use it.
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.
I don't actually have a lot of experience with the rust world, but the main complaint with macros is typically that you can't step through them easily.
With my getting downvoted, I'm assuming I'm wrong and that step debuggers are more common in rust than I was assuming. I think that makes me happy. If anyone has a good video demonstrating this level of tooling, I'd be interested.
Some of it may be situational. With how many microservices I'm dealing with, "stepping into" a call really needs to jump to another process quite often.
Anyone have numbers, by chance, on how many folks actually use a lot of these features?
Usually the problema is that many developers never learned what a visual debugger is capable of, and the typical command line interface in UNIX leaves little room for exploration if people aren't curious to learn about them.
For example, many aren't aware that gdb has a TUI that eases the experience.
But, really, this is something I've never actually done. Reasoning about how the system should behave has been enough to get me by.
When some condition took place, the problematic process would break into the debugger, while the others would be parked waiting for me to attach to them.