Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
A quick primer on type traits in modern C++ (internalpointers.com)
118 points by tblr on Feb 29, 2020 | hide | past | favorite | 34 comments


The amount of background knowledge you need to understand what’s happening here... I don’t envy people who have to catch up

becoming more and more convinced that when sfinae opened the Turing hatch it doomed c++


I'm not sure, I last coded C++ about 8 years ago and that was all C++03, yet I had no trouble to follow along.

I mean, even if you never saw a "constexpr" before, isn't it pretty clear from the example and the name, well, "constexpr" want's going on? Same for the other newish C++ features.

I thought it was a nice little primer.


> The amount of background knowledge you need to understand what’s happening here...

Not a whole lot, actually. You can go from C to this in about a week if you’re focused. (I did this a few years ago when learning C++. Now if you want to talk about initialization, that’s a whole ‘nother story…)


> …when sfinae opened the Turing hatch it doomed c++…

I think that’s unfair. While “sfinae’ itself is a mouthful, what it says in practice is that template expansion observes the principle of “least surprise” which is inherently user friendly.

You certainly don’t need to know the acronym, or even of the idea, to benefit from it. I stead, the developer won’t be presented with a confusing error message when the thing they want to happen is supported but not the first possibility examined.


I just learned about sfinae by googling it, so correct me if I'm wrong, but I think you're misreading the GP's comment.

The argument is I think, that because sfinae effectively added an "if" statement to C++ templates, it made templates turing complete. It made it possible to write all kinds insane, undebuggable programs purely at compile-time, in the most terrible purely functional language imaginable. People did exactly that, which went fine until their colleagues (or users of open source libraries) did something unexpected and got screenfuls of errors.

Wikipedia's sfinae page has a nice story about how it added compile-time introspection and conditionals to the language: https://en.wikipedia.org/wiki/Substitution_failure_is_not_an...


Any powerful language (lisp, c++) has plenty of sharp knives in its toolbox. My point was that this tool made it easier to write code that behaves the way the end user expects. Primarily library writers (c++ is full of weirdo constructs that I never use in my code but which people writing libraries use to valuable effect — and which make my code better). While people complain that “c++ has this hard to understand feature” the real question is “does that feature make writing user code easier” and the committee (in ‘17 and ‘20) I hunk have taken that approach.

Sure you can write spaghetti code, but you can do that in almost any language. And there are popular languages that had explici design goals of limiting expressiveness in the hope of reducing spaghetti code (Pascal, in its time, and go today). I consider it like a balloon: without that power you end up with more boilerplate and repeated code, which to me makes following the code harder. But other people reasonably disagree.


I think there's a point in rjeli's and skrebbel's comments though. This wasn't designed in from the get go; it started as a hack that got papered over with abstractions and accepted as a standard practice. Despite later improvements, the whole thing still has the aftertaste of a clever hack.

Using a modern example, the way template metaprogramming started feels like return-oriented programming, or those "MOV is Turing-complete" or "MPU error handling is Turing-complete tricks". Someone figured out the standard unintentionally lets you make a conforming compiler do computations, and it ended up becoming an accepted feature, instead of updating the language to something resembling modern constexprs, or Common Lisp's compiler macros.


> ...all kinds insane, undebuggable programs purely at compile-time ... in the most terrible purely functional language imaginable

It's a clean, minimal, purely functional Lisp-like language. Nothing 'insane' or 'undebuggable' about it.

(Well, maybe if you're the kind of person who only ever coded Qt-style OOP C++98 in your life you might be shocked, but for the rest of us there is nothing surprising or special about C++ templates.)


Having worked a lot in both C++ and Lisp, I object to calling C++ templating "clean" and "minimal". C++ is a mess. I still love it though.


AFAIR Turing hatch was opened before SFINAE; I remember playing with early template metaprogramming examples as a kid (computing a factorial, etc.) which amounted to what the adult me now recognizes as Lisp's cond, that is generalized if-elseif-else. SFINAE only added more places into which you could shove compile-time conditionals.


You don’t need sfinae to get Turing completeness of templates. Template specialization (a crude form of pattern matching) is the main driver for Turing completeness. For example, here’s a toy project to implement a subset of scheme lisp in templates which needs no sfinae: https://github.com/tdp2110/TmpLisp


I seriously don't understand this. I am a developer learning Modern C++ and this article was perfectly clear to me. I was using `enable_if` before, but the new `if constexpr` does make things clear and idiomatic.


I think that it's pretty easy if you just consider that what's happening between `template<` and `>` is a simple LISP whose values are types.


more like a prolog with obtuse syntax and complicated semantics


Scott Meyers' next Effective C++ is going to be a doozy. Is it even possible to usefully categorize all the edge cases anymore?


Scott Meyers has retired from C++ (his own blog post: http://scottmeyers.blogspot.com/2015/12/good-to-go.html?m=1) so the next C++ bible has to come from somewhere else. Maybe he saw it coming :)


traits + "constexpr if" replace SFINAE.


You often need sfinae to implement the trait itself, but concepts (and the 'requires' keyword itself) will finally get rid of sfinae.


It just looks kinda wrong to me - If your generic template has to switch on what type it's been passed, surely you've made it too generic somewhere? Adding back conditional compilation or execution afterwards strikes me as the same sort of code-smell as checking if something is an instance of a given class before then casting it and calling a member - you've lost information and using some sort of reflection to get it back looks like poorly thought out code. Sure, there are times when you more or less have to, but you ought to try to avoid them.

But what do I know, I haven't done C++ for about 5 years and was never that into it.


You do not switch on types, you switch on groups of tuoes having certain properties. It is not a new thing this sort of thing has been doable for the last 20 years in a form or another.


I have an example for you. A serialization library that supports container classes. It has a generic way to fill lists (vectors), but not all list containers support resize and default-initialize new elements. So somewhere inside a generic template function there is a switch to use or replicate that function.


Think of it the other way around: there’s a generic method/template, but the developer has a special implementation for a specific case. The compiler already does this for things like memmove() that is properly aligned and sized; this allows user code to cleanly do the same thing.


That was an excellent article, thank you.


I found the use of 'if constexpr' in the example pretty nasty. I would have thought it more idiomatic to use std::enable_if to enable different implementations of 'algorithm' as in the example of 'construct' and 'destroy' given here:

https://en.cppreference.com/w/cpp/types/enable_if


> I found the use of 'if constexpr' in the example pretty nasty.

For new code it is to be preferred - it generates less symbols than enable_if, compiles faster (by a noticeable margin from my own experience) and makes for more readable code as you don't have to go look everywhere for the various enable_if cases.


Enable if is fine if you have mutually exclusive choices (although the way it work is pure magic), but it quickly becomes very complex for overlapping cases as you have to somehow hack the partial ordering in. A chain of constexpr if is significantly easier to write and understand.

The right solution are of course concepts.


Most of this is pretty new to me. Why is it nasty? It looks neat to me!


I consider it nasty (compared to the std::enable_if example that I linked) because it does not express the programmer's intent.

When you use enable_if to enable specific algorithm variants for specific trait predicates, you're saying "here is the specialised algorithm to use when this predicate is satisfied". Both the predicate and the algorithm code that relies on the predicate being satisfied are part of the template specialization definition. There's no chance of missing a precondition: it's stated right in the definition. Another advantage is that it's completely modular -- you can add or remove specializations at will.

On the other hand, the example in the post using 'if constexpr' wraps the algorithm in an extra layer of indirection (a generic wrapper) with a bunch of constexpr if-else spaghetti used to dispatch to the correct implementation. Even if the if-else spaghetti is pristine, the fact remains that the predicate and the specialized code that depends on the predicate as a precondition have been separated out into two independent code sites.


algorithm_signed() and algorithm_unsigned() in the conditional compilation example need to be templated functions for the example to make (more) sense.


Is algorithm being templated not enough?


Two points. 1) Currently, the code could be written with normal overloading (one for int and one for unsigned). No traits required. 2) The current code relies on an implicit cast to int or unsigned int. I’m not sure that’s good.


> Currently, the code could be written with normal overloading (one for int and one for unsigned). No traits required

f( (uint8_t)0 ) will dispatch to the overload f(int) with overloads. The type-trait code will dispatch to f_unsigned(unsigned).


I didn't believe you but you're right. The uint8_t gets promoted to int, not unsigned int.

https://godbolt.org/z/icQHw3


C’s integer promotion rules aren’t the best.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: