Sort of, in that they are a thing you do to constrain generic parameters.
A significant difference in my understanding is that type classes and traits are required, but concepts are not. That is, using the example from the article, a concept can tell you if you're passing something that doesn't add, but you can add inside a function without using a concept. In other words:
fn add<T>(x: T, y: T) -> T {
x + y
}
This won't compile in Rust:
error[E0369]: cannot add `T` to `T`
--> src/lib.rs:2:7
|
2 | x + y
| - ^ - T
| |
| T
|
help: consider restricting type parameter `T`
|
1 | fn add<T: std::ops::Add<Output = T>>(x: T, y: T) -> T {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
This suggestion works, but you don't have to write it this way. Once the constraints get more complex than T: Foo, I personally switch to this form:
use std::ops::Add;
fn add<T>(x: T, y: T) -> T
where
T: Add<Output = T>,
{
x + y
}
I find it a little easier to read. YMMV.
Whereas in C++, this does compile:
template<typename T>
T add(T a, T b)
{
return a + b;
}
If you try to add something that doesn't have + defined:
int main(void) {
add("4", "5");
}
you get this
<source>:4:12: error: invalid operands to binary expression ('const char *' and 'const char *')
return a + b;
~ ^ ~
<source>:8:5: note: in instantiation of function template specialization 'add<const char *>' requested here
add("4", "5");
^
Whereas, if you do what the article does (though I'm using char* instead of std::string, whatever)
#include <concepts>
template<typename _Tp>
concept integral = std::is_integral_v<_Tp>;
template<std::integral T>
T add(T a, T b)
{
return a + b;
}
int main(void) {
add("4", "5");
}
you get
<source>:12:5: error: no matching function for call to 'add'
add("4", "5");
^~~
<source>:6:3: note: candidate template ignored: constraints not satisfied [with T = const char *]
T add(T a, T b)
^
<source>:5:15: note: because 'const char *' does not satisfy 'integral'
template<std::integral T>
^
/opt/compiler-explorer/gcc-11.1.0/lib/gcc/x86_64-linux-gnu/11.1.0/../../../../include/c++/11.1.0/concepts:102:24: note: because 'is_integral_v<const char *>' evaluated to false
concept integral = is_integral_v<_Tp>;
^
This doesn't feel like a huge change because add is such a small function, but if it were larger and more complicated, the error with a concept is significantly better.
A significant difference in my understanding is that type classes and traits are required, but concepts are not. That is, using the example from the article, a concept can tell you if you're passing something that doesn't add, but you can add inside a function without using a concept. In other words:
This won't compile in Rust: This suggestion works, but you don't have to write it this way. Once the constraints get more complex than T: Foo, I personally switch to this form: I find it a little easier to read. YMMV.Whereas in C++, this does compile:
If you try to add something that doesn't have + defined: you get this Whereas, if you do what the article does (though I'm using char* instead of std::string, whatever) you get This doesn't feel like a huge change because add is such a small function, but if it were larger and more complicated, the error with a concept is significantly better.