[HN Gopher] C++20 Concepts: The Definitive Guide
___________________________________________________________________
C++20 Concepts: The Definitive Guide
Author : emma-w
Score : 116 points
Date : 2021-08-22 15:45 UTC (7 hours ago)
(HTM) web link (thecodepad.com)
(TXT) w3m dump (thecodepad.com)
| contravariant wrote:
| I guess this is somewhat besides the point but checking if
| something can be multiplied by -1 is not great as a definition of
| subtraction. You'll typically want to use the additive inverse
| directly.
|
| 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.
| godelski wrote:
| Why would this be an issue? I'm under the impression that this
| is fine if we're operating with the standard addition operator
| and the standard field that everyone is used to working under.
| Isn't that the definition of the inverse? I understand that in
| different fields you have different operators but is that
| relevant here?
| contravariant wrote:
| If you were working with the standard field then why would
| you need to bother defining the general concept of
| subtraction?
|
| 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).
| godelski wrote:
| I mean but under the example that the author is doing they
| are working with standard integers (integrals). So this
| appears to me to still be the standard addition and
| subtraction. And with standard R1 subtraction it is the
| same as inverse of addition.
|
| I mean if we move into different coordinates and different
| fields, then yeah things change, but I don't see what the
| issue is with the example given here.
| MauranKilom wrote:
| Random example: In geometry code, you might distinguish
| between points and vectors, where point + vector = point,
| point - point = vector etc.
|
| 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).
| godelski wrote:
| But in this case I don't think you're working on the
| standard field. I mean you're working in different
| coordinates. I do understand that the example only works in
| R1 space with standard addition, but that's kinda the point
| of my question. That the "error" isn't really an error
| unless you're being pretty pedantic.
| google234123 wrote:
| Amazing that this took ~20 years to design and it still had to be
| rushed through to get into c++20...
| Koshkin wrote:
| You can take K&R C from my cold, dead hands.
|
| 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.)
| 37ef_ced3 wrote:
| I think you're trying to say you like C but dislike C++'s
| complexity.
|
| You will enjoy Go.
|
| Go can be understood as an improved, modernized C that
| doesn't abandon C's simplicity.
| throw_m239339 wrote:
| > Go can be understood as an improved, modernized C that
| doesn't abandon C's simplicity.
|
| This is false, because Go has a garbage collector by
| default. This isn't an "improvement" in anyway but for
| those who don't care about memory management and
| predictable and deterministic performances.
| the_duke wrote:
| The "Go is like C" comparison never made any sense to me.
|
| Go has a sophisticated runtime with transparent N:M
| threading and built-in concurrency primitives, Interfaces,
| garbage collection and a large standard library.
|
| Go is only simple when compared to the other languages that
| sit in a similar space, like Java and C#.
| pjmlp wrote:
| C's runtime is UNIX, that is why we got POSIX.
| eeegnu wrote:
| Something about type constraining auto just seems funny. I can
| see how it's useful from an error minimization / code clarity
| standpoint, but it almost seems counter to the point of even
| using auto.
| gmueckl wrote:
| I belive that you do not sufficiently understand the giant
| footgun that is unconstrained auto, especially in the context
| of very template-heavy code. Concepts solve the issue that
| judicious use of auto will allow template instantiations to
| succeed that are plain wrong in that they will happily do the
| wrong thing just because the types involved fit the constraints
| by mere chance and not because they were meant to be used
| together like this.
| ncmncm wrote:
| Unconstrained auto is unwise in globally accessible templates
| that participate in overload resolution, but is fine in
| lambda literals.
| gpderetta wrote:
| Initially auto was not required (nor allowed). The concept name
| itself was enough. Adding auto is one of those compromises that
| please no one but was necessary to move the proposal forward
| Razengan wrote:
| I'm sorry for this petty criticism, but every time I gaze upon
| "modern" C++ I wonder if the language could get any uglier, and
| with every revision it somehow manages to.
| tialaramex wrote:
| I feel like one of the things a "Definitive Guide" to this
| feature needs to make clear, and maybe even emphasise, is a key
| way these are different than say Rust type Traits.
|
| 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.
| secondcoming wrote:
| > I feel like one of the things a "Definitive Guide" to this
| feature needs to make clear, and maybe even emphasise, is a key
| way these are different than say Rust type Traits.
|
| What use would that be to a C++ developer who doesn't know
| rust?
| CodeMage wrote:
| > _I feel like one of the things a "Definitive Guide" to this
| feature needs to make clear, and maybe even emphasise, is a key
| way these are different than say Rust type Traits._
|
| > _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.
| svalorzen wrote:
| Are you sure about this? In my tests floating points are always
| considered partially ordered, not totally ordered. This page
| [0] even mentions this in the notes towards the bottom.
|
| [0]:
| https://en.cppreference.com/w/cpp/utility/compare/partial_or...
| tialaramex wrote:
| Note that std::totally_ordered is a concept (this topic is
| about "C++ 20 Concepts: The Definitive Guide") whereas you're
| talking about std::partial_ordering which is a class, also
| introduced in C++ 20.
|
| Specifically these ordering classes are the result of the
| spaceship operator and the _concept_ doesn 't care whether
| you have a spaceship operator.
| robocat wrote:
| <=> spaceship operator: Good article on the C++20 three-way
| comparison operator:
| https://devblogs.microsoft.com/cppblog/simplify-your-code-
| wi...
| banachtarski wrote:
| How can floats be totally ordered. This isn't even a matter of
| NaNs or not. A set of floats where two or more floats compare
| equal does not permit a total ordering.
| quietbritishjim wrote:
| I think that's the parent comment's point. Floats are not
| totally ordered but C++'s type system is weak enough that it
| appears they are.
| jcelerier wrote:
| IEEE754 provides a total ordering algorithm actually, but
| it's not used when you do double a, b; ... a < b
| thewakalix wrote:
| What do you mean? I'm pretty sure no two distinct floats
| compare as equal.
| jeffbee wrote:
| -0.0 and +0.0?
| CogitoCogito wrote:
| Are those floats actually distinct? I mean you could
| represent 3.0 + 4.0 = 7.0 as well. Are "-0.0" and "0.0"
| actually different?
|
| (This is a serious question I honestly don't know.)
| ncmncm wrote:
| They are distinct values but they compare equal. For
| almost all uses they are effectively equal, except where
| you are producing infinities.
| CogitoCogito wrote:
| I hadn't thought about the infinities part. If they had
| the same behavior in all operations involving other
| numbers, then I could see how they could nonetheless be
| the same from a C++ type perspective, but given they
| behave differently, C++ certainly couldn't consider them
| the same.
|
| Thanks for clearing that up!
| titzer wrote:
| -0 and 0 do.
| s-luv-j wrote:
| Is it just me, or have C++ errors gotten a lot better? Below
| example from guide seems a lot more ergonomic than in years past.
| 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).
| tialaramex wrote:
| There has been more interest from modern programming languages
| and modern compiler developers in good diagnostics in two ways:
| 1. The earlier you report the problem, the cheaper the fix and
| 2. The better the diagnostic the more likely the programmer's
| fix is appropriate to the actual problem they had.
|
| 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.
| laszlokorte wrote:
| One of the major motivations for concepts is to allow better
| error messages.
| google234123 wrote:
| And ironically it's questionable if it helps. Worse error
| messages were one of the main reasons it got blocked from 17.
| afranchuk wrote:
| Yeah, and besides the focus on error messages in past years,
| concepts will naturally allow template errors to be a _lot_
| clearer, since they can serve as the specification of templated
| type requirements.
| secondcoming wrote:
| Yes. I had build failures related to LTO on our production code
| that uses gcc 7.5 (Ubuntu 18.04). I had to build it with gcc
| 9.1 (Ubuntu 20.04) in order to get a useful error message that
| saved me an hour or two of faffing about.
| lamp987 wrote:
| That has nothing to do with C++. Its entirely a compiler thing.
| pharmakom wrote:
| True, although the distinction hardly matters to users.
| wk_end wrote:
| Not entirely true - one of the motivations for new features
| and libraries C++ adds is to make better errors possible. In
| this case, `static_assert` was added in C++11, and
| `is_integral_v` was added in C++17. Concepts, the new feature
| this post is about, falls under that category as well.
| agumonkey wrote:
| clang kinda started the race
| pjmlp wrote:
| And now lags behind in C++20 support, as apparently not
| everyone is keen to upstream whatever they are doing.
|
| Clang concepts were implemented by one guy.
|
| https://cppcast.com/saar-raz-clang-hacking/
| Someone wrote:
| One of clang's stated goals is "expressive diagnostics"
|
| 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.
| bool3max wrote:
| The link leads to a 404.
| Someone wrote:
| Thanks; HN thought the semicolon was part of the URL. Added
| a space.
| leni536 wrote:
| It's not just you, dinostics continuously and noticably
| improve. A lot of effort is spent on this.
| ncmncm wrote:
| The most important thing to say about this "definitive guide" is
| that it delays to the end presenting the overwhelmingly most
| important detail about Concepts: how to use them.
|
| 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.
| CodeMage wrote:
| > _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.
| [deleted]
| creativeCak3 wrote:
| Am I going insane or does the following does NOT work(?):
|
| <code> template<typename T> T mul(T a, T b) { return a _b; }
|
| 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 a _b; }
|
| 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??
| pharmakom wrote:
| Is this a scrappy version of type classes (Haskell) or traits
| (Rust)?
| steveklabnik wrote:
| 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.
| nrclark wrote:
| Another language-changing paradigm added to C++. I haven't even
| finished learning the old ones yet.
|
| Sometimes I think C++ would be better off if the committee
| stopped accepting proposals that add new features.
| jcelerier wrote:
| > Another language-changing paradigm added to C++
|
| 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)
| steveklabnik wrote:
| While on one hand, I hear you, but on the other, on some level,
| the design goes all the way back to 1994:
| https://programowaniezpasja.pl/wp-content/uploads/2019/05/Cp...
| pjmlp wrote:
| Just like Java, VB and C# are getting theirs.
| gumby wrote:
| I think there's more to it (changing the language) than you are
| crediting it.
|
| 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.
| gompertz wrote:
| I think you hit it on head, that most new features are for
| library writers who need to capture every edge case
| succinctly. As someone who has used c++98 and only a little
| c++11, for nearly 15 years, and writes no libraries; I've had
| little real need for any new features.
| ncmncm wrote:
| The less you use the new features, the less benefit you get
| from them. You could stick to K&R C and get no benefit at
| all, but that would be equally as foolish as what you are
| doing.
|
| The new features are there to improve your experience, and
| to make your code more reliable. When you have a choice
| between old and new, new is usually better.
|
| Sticking to K&R, you would have as many bugs and crashes as
| other K&R code. Sticking to modern C++ makes most of such
| bugs impossible. That is progress.
|
| You can _stop learning and take up complaining_ at any
| time, as you have done.
|
| But you can also _start learning again_ at any time. Now is
| always a good time for that.
| robocat wrote:
| You don't know what domain the person you are answering
| works in, so you cannot make such sweeping statements.
| Implying someone is foolish is plain rude.
| https://news.ycombinator.com/newsguidelines.html
| gompertz wrote:
| I'm not sure how you drew a line from my discussion of
| c++98/11, to K&R C.
| in3d wrote:
| It's true that C++ is huge but the complaint should be about
| the lack of epochs that would let us simplify the language
| instead of about very useful new features such as concepts,
| modules, or constexpr. If you're already programming in C++ and
| you can't take a few days every 3 years to learn about new
| additions, you'll have even more problems with other languages.
| gpderetta wrote:
| Concepts have existed informally since the original C++ STL.
| For the last 25 years people have been trying to make them a
| language construct. The current design is the one compromise
| people could agree on while being implementable and backward
| compatible.
|
| I haven't used it much yet, but I would still say it is pretty
| good although far from a proper template type system.
| AlexCoventry wrote:
| Scott Meyers thinks similarly, I think.
| https://www.youtube.com/watch?v=KAWA1DuvCnQ
| ncmncm wrote:
| Scott Meyers was never a production coder. His schtick was
| teaching, and he got tired.
|
| The additions since he bowed out improve the experience of
| production coders.
| AlexCoventry wrote:
| He got tired because of the insane complexity of the
| language, and in the video I linked he supports his
| complaint with extensive and damning examples.
| FpUser wrote:
| C++23 vs C++20 seem to be less drastic. So maybe they will slow
| down to a slow trickle at some point ;)
| heresie-dabord wrote:
| > Another language-changing paradigm added to C++
|
| 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.
___________________________________________________________________
(page generated 2021-08-22 23:01 UTC)