[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)