Concepts, just like the SFINAE and constrexpr hacks you should discard in their favour, are about only what will compile and you, the C++ programmer, are always responsible for shouldering the burden of deciding whether that will do what you meant even if you have no insight into the types involved.
Example: In C++ the floating point type "float" matches a concept std::totally_ordered. You can, in fact, compile code that treats any container of floats as totally ordered. But of course if some of them are NaN that won't work, because NaNs in fact don't even compare equal to themselves. You, the C++ programmer were responsible for knowing not to use this with NaNs.
Whereas, Rust's floating point type f32 implements PartialOrd (saying you can try to compare these) but not Ord (a claim that all of them are totally ordered). If you know you won't use NaNs you can construct a wrapper type, and insist that is Ord and Rust will let you do that, because now you pointed the gun at your foot, and it's clearly your fault if you pull the trigger.
This is a quite deliberate choice, it's not as though C++ could have just dropped in Rust-style traits, but I think a "Definitive Guide" ought to spell this out so that programmers understand that the burden the concept seems to be taking on is in fact still resting firmly on their shoulders in C++.
The other side of this is, if you wrote a C++ type say Beachball that implements the necessary comparison operators the Beachball is std::totally_ordered in C++ 20 with no further work from you to clear up this fact. Your users might hope you'll document whether Beachballs are actually totally ordered or not though...
I think this will likely prove to be a curse, obviously its proponents think it will work out OK or even a blessing.
> Concepts, just like the SFINAE and constrexpr hacks you should discard in their favour, are about only what will compile and you, the C++ programmer, are always responsible for shouldering the burden of deciding whether that will do what you meant even if you have no insight into the types involved.
To be fair, that's not what makes them different from Rust. Unless there's something in Rust that I missed, it offers no guarantees that the implementation of the trait is consistent with its semantics.
Whether it's traits or concepts, it's still about compile-time type-checking, not actual contracts.
So in C++ the fact a Mouse is food::LovesCheese doesn't actually tell me whether the Mouse's programmer has any idea what it means to food::LovesCheese or whether the food::LovesCheese programmer knows about a Mouse. I need to carefully read the documentation, or the source code, or guess. It might be a complete accident, or at least an unlucky side effect.
In Rust the fact a Mouse has the trait food::LovesCheese always means specifically that either (1) the Mouse programmer explicitly implemented food::LovesCheese as part of a Mouse or (2) the food programmer explicitly implemented a way for Mouse to food::LovesCheese. Rust requires that nobody but them can do this, if I have a Mouse I got from somewhere but alas it doesn't food::LovesCheese and I wish it did, I need to build my own trivial wrapper type MyCheeseLovingMouse and implement the extra trait.
Either way as the user of any Mouse, you can be sure that the fact it has food::LovesCheese in Rust is on purpose and not just an unfortunate behaviour that nobody specifically intended and you need to watch out for.
Bearing this in mind, neither concepts in C++ nor traits in Rust guarantee any semantics in the Rust sense of the word. In the C++ sense of the word, C++ concepts do carry guarantees, in that the spec says what they should do. But, in this sense, so does Rust: PartialEq [1], for example, has semantic requirements spelled out in the documentation. In my mind, the difference is that Rust programmers tend to program defensively, not trusting programmers to get things right that the compiler doesn't enforce. Thus you see a lot of conversations along the lines of "what if the implementer of trait X does something weird?" in the Rust space. This may give the impression that Rust traits don't have a clear and consistent semantics associated with them. But that's not right: the implementer of PartialOrd, for example, is absolutely expected to implement a proper partial order, as explained in the documentation. A specification for Rust could specify associated semantics for those traits, just like in C++.
[0]: https://en.cppreference.com/w/cpp/utility/compare/partial_or...
Specifically these ordering classes are the result of the spaceship operator and the concept doesn't care whether you have a spaceship operator.
What use would that be to a C++ developer who doesn't know rust?
As you see from the standard library concepts the emphasis is on semantic claims like "fully ordered" but this feature does not actually provide semantics and I think that's a trap programmers would be likely to fall into.
template <typename T>
struct totally_ordered : std::false_type {};
and opt-in implement it for some types, but not for floats.Then you can define a TotallyOrdered concept that requires the trait.
The C++ standard library didn't do this, so if you happen to accidentally implement a type with an API that conforms to TotallyOrdered, then it becomes "accidentally" TotallyOrdered, which is a big footgun.
If your Delicious concept mistakenly applies to my Desert, how do I as the author of the Desert tell C++ "No, no, when people ask if a Desert is Delicious tell them it isn't?".
test.cpp: In instantiation of ‘T add(T, T) [with T = std::basic_string<char>]’:
test.cpp:17:21: required from here
test.cpp:11:22: error: static assertion failed
11 | static_assert(std::is_integral_v<T>);
| ~~~~~^~~~~~~~~~~~~~~~
test.cpp:11:22: note: ‘std::is_integral_v<std::basic_string<char> >’ evaluates to false
Build finished with error(s).See https://clang.llvm.org/diagnostics.html ; that page is not dated, but compares to gcc 4.9, which is from April 22, 2014.
gcc also has worked on improving its error messages (most likely because of competition with clang), so that comparison probably isn’t accurate anymore.
I agree this is great news. I actually had to write MS SQL for the first time last week and it was disappointing but sadly expected to have it respond to a common but not technically-standard SQL syntax I'm used to with "Syntax error" as though somehow that's helpful. Such poor errors meant I spent more time reading the documentation than writing queries even though I've years of experience across several other SQL dialects.
Clang concepts were implemented by one guy.
The right place to put a concept name, in production code, is in place of "typename" in a template definition, or even better in place of "T" in the function argument declaration. That is, instead of
template<typename T>
T add(T a, T b)
requires addable<T> (
return a + b;
)
say template <addable T>
T add(T a, T b) {
return a + b;
}
or auto add(
addable auto a,
addable auto b) {
return a + b;
}
according as whether you want to enforce a and b to have the same type (which is another omission).Most often it is not necessary, and not wanted, to enforce a and b having the same type.
That really depends on what you're trying to do. Presenting these two different declarations as somehow equivalent is very misleading and I'm glad that the author didn't do that.
The author's failing was in presenting neither of them until long after they were due, and presenting thoroughly inferior alternatives in the meantime.
Sometimes I think C++ would be better off if the committee stopped accepting proposals that add new features.
First, you don't have to use the new features (though eventually you'll be reading the code of people who did, so this is only half-valuable). There is new c++11 code being written every day -- in volume (a hard to pin down amount) it's sadly more than 50%. The usage surveys don't really capture this clearly (and it's not clear they could).
Second: often new features are for library writers, or are out there for library writers to use (e.g. coroutines, which probably will not be appropriate for many users before c++23, but pretty much need to be available for people to experiment with).
Third: the new features tend to be additive. For example you don't need to use many of the stuff in <algoritm> -- stick to a for loop unless you want to take advantage of some new capability (e.g. policies, which are't ubiquitous). Concepts are the same way: they will improve error messages and reduce bugs, but if you don't use them your code will in 99.9% work just fine. When you see a very simple example that uses concepts, it's not surprising that the concepts don't really improve a simple add function -- the case is deliberately simple for explanatory purposes.
Languages move forward. Even go recently caved and added in generics.
But it complicates the compiler, complicates the tools, complicates the error messages,...
Take something as "simple" as operator overloading. If a beginner does a "5"+5, the compiler is forced to respond with "no operator + for the types given, types are: int, std::string" instead of the infinitely more friendly "can't add string to int". I actually _like_ operator overloading, I wish more languages stopped the silly practice of making language-provided primitives somehow sacred and closed-to-extension. But look at how even this relatively straightforward and useful feature ruined the error message.
Part of this particular example is bad error message design. I mean, what are the chances somebody intended to do operator overloading but then forgot to implement the method? It would have been much better to say "Can't add string to int, Did you forget to implement operator+(.. .,...)?". This centers the common case first, then reminds the experienced about what could have happened in their case. But even this might confuse beginner and convince them that "5"+5 is somehow a reasonable thing to say (because it actually is in some contexts) that they just need to get that fabled operator+ from somewhere to make it work, instead of just outright telling them " nonsense, not gonna work".
Extensions are always, inherently, abstractly, a tradeoff. Because even if you are a perfectly spherical coder who doesn't interact with other code in any way, you at least interact with the language tools. And the tools _must_ know all the of the language: instead of just having the luxury to say "sorry mate can't do that" when encountering "5"+5, the editor/IDE/compiler has to spend the cycles to see if it _can_ do that, then report a message that simultaneously says that you can but can't. As the features accumulate and interact, the tradeoff curve increasingly inflect against adding new things.
All of this in the abstract is a fairly balanced argument that applies to any language, the particular case of C++ is much, much worse. C++ have this aweful way of "retconning" new features. I can never put my hands on it, but C# creators practically design a new superset every major version and call it the same name without ever making people angry or afraid that the language is spiraling out of control, but every C++ feature gives me this dark feeling of "is this never ever going to end? how far are you willing to take it?".
Part of that is undoubtedly the syntax, remind me again what language class (regular, context free,..) is C++ syntax? The other day I was fooling around and thought of adding discriminated unions to C++, just a thin syntax layer over a more verbose idiom. The very first thing is to parse the damn thing, but that way lies madness. Because C++ have no agreed-upon, guaranteed formal grammar. There is a grammar online but it's gargantuan and was written by one man, there is a grammar in the standard but it's gargantuan and the standard says it's there for illustration only, there is a grammar (implicitly) in the source code of any working parser but it's gargantuan and full of hacks. So for all practical intents and purposes, C++ has no formal grammar. Think about that for a minute, a formal language that has no formal grammar. It's the bare minimum any formal language can have, and yet you can't do it safely because of the massive, beast-like bloat that is that language.
Rather than something as coarse as epochs, C++ includes a lot of fine grained feature test preprocessor definitions such as (to pick one at random) '__cpp_lib_constexpr_algorithms'.
It's harder to make this work with breaking API changes of course. I assume non-preprocessor versions that work with modules will be available in C++23...and perhaps they will support API changes?
no, people were writing "concept-like" code since C++98 in a very bloated way with e.g. sfinae, this is just standardizing existing practice (while improving it of course)
I haven't used it much yet, but I would still say it is pretty good although far from a proper template type system.
I agree. Somewhere in the piled-high-and-deeper complexity of C++ there is one excellent, modern language that could be carved out.
Maybe it is only the subset since C++17.
The additions since he bowed out improve the experience of production coders.
They are not though - the equivalent C++17 would be
template<typename T>
std::enable_if_t<std::is_intregral_v<T>, T> add(T a, T b)
{
return a + b;
}
The difference is that these make the function unavailable for overload resolution for non-integral T but another implementation might still cover them while the static_assert version leaves the function available for overload resolution but then errors if it is selected.I mean sure you can extend any commutative group into a module over the integers but you probably don't want to make this a hard requirement just to have subtraction.
It can then also be convenient to have a special type/value Origin, which (while functionally identical to point(0, 0)) allows you to e.g. write vector = point - origin to clearly express your intent of turning a vector into a point.
-1 * origin is not meaningful, but point - origin is (while point + origin is not).
If you want to define the concept of subtraction then you probably don't want to assume you can multiply elements with an integer. Not that it can't be done but in general it will be a lot easier to define the additive inverse directly (if one exists).
<code> template<typename T> T mul(T a, T b) { return ab; }
template<typename T> T mul(T a, int b) { std::string ret_val{std::move(a)}; a.reserve(a.length() b); auto start_pos{a.begin()}; auto end_pos{a.end()}; for(int i = 0; i < b; i++) { std::copy(start_pos, end_pos, std::back_inserter(a)); } return ret_val; } </code>
I knew it looked odd to me for some reason...I had to re-write it as follows:
<code> template<typename T> T mul(T a, T b) { return ab; }
template<typename T> T mul(T a, int b) { std::string ret_val{}; ret_val.reserve(a.length() b); auto start_pos{a.begin()}; auto end_pos{a.end()}; for(int i = 0; i < b; i++) { std::copy(start_pos, end_pos, std::back_inserter(ret_val)); } return ret_val; } </code>
Did I miss something??
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.There was no header file hell. Often you didn't need a single header to be included in your code: most functions returned int (or nothing), and if you needed something that returned double, you could just say so.
I remember being excited about function prototypes, but something was irretrievably lost at that point. The primal elegance of C as it was conceived by its creators is long forgotten now.
(If you want, you can still experience it with the Tiny C Compiler that seems to continue to understand K&R C code just fine.)
You will enjoy Go.
Go can be understood as an improved, modernized C that doesn't abandon C's simplicity.