
C++20 Ranges - ot
http://ericniebler.com/2018/12/05/standard-ranges/
======
Sharlin
At first I wondered why he calls the monadic flatmap operation "for_each" but
it makes sense in that he's actually creating a (lazy) list comprehension DSL
here. The code within main() is mostly equivalent to the following Python
code:

    
    
      triples = ((a, b, c) for c in itertools.count()
                           for a in range(1, c+1)
                           for b in range(a, c+1)
                           if a*a + b*b == c*c)
    
      for (a,b,c) in itertools.islice(triples, 10):
          print("{}, {}, {}".format(a,b,c))
    

The equivalent in Rust, just because I'm currently learning it:

    
    
        let triples = (1..)
            .flat_map(move |c| (1..=c)
                .flat_map(move |a| (a..=c)
                    .flat_map(move |b| Some((a, b, c))
                        .filter(|_| a*a + b*b == c*c))));
        
        
        for (a,b,c) in triples.take(10) {
            println!("{}, {}, {}", a, b, c)
        }

~~~
blub
I was anyway thinking that his example looked incomprehensible, but your
Python example makes a complete mockery of it.

The difference is night and day, I'm dismayed at how horrible that C++ code
is, hope I never have to deal with such crap.

~~~
mehrdadn
> but your Python example makes a complete mockery of it.

I mean, try getting the same kind of performance and static verification in
Python?

So much of the complexity of C++ is due to the static typing and the
optimizations that can come with it. If you're willing to give up and make
everything dynamic then of course it'll simplify your code, but you get a
massive penalty both in terms of performance and in terms of error-checking.

~~~
Sharlin
Upvoted, but to be fair, Rust shows that expressiveness and performance aren't
mutually exclusive. It does not (currently) have language-level list
comprehensions, but those could be easily enough implemented as simple
syntactic transformations over the existing iterator combinators, the way
Scala does it.

~~~
mehrdadn
Thanks, yeah, I was very careful to avoid claiming they're mutually exclusive.
However, although I stopped trying to learn Rust back in 2016, unless it's
changed its fundamentals radically (which I understand it hasn't), I'm not
sure if it's really a counterexample here for expressiveness. Can you use
different allocators for different vectors in the same function in Rust? Can
you have custom move semantics for objects that might know about pointers to
them? And does the compiler no longer complain about code that really _should_
work just fine? [1] I understand the answers to most if not all such questions
is still "no", at which point I stop believing it really has the same
flexibility as C++ or the same ease of writing as Python, but would be happy
to hear if things have changed.

[1] [http://softwaremaniacs.org/blog/2016/02/12/ownership-
borrowi...](http://softwaremaniacs.org/blog/2016/02/12/ownership-borrowing-
hard/en/)

~~~
couchand
> And does the compiler no longer complain about code that really should work
> just fine?

In the general case that's theoretically impossible, and also not really a
good measure anyway.

~~~
mehrdadn
Okay, how about in the practical cases that actually come up and make you have
to work around the compiler?

~~~
Sharlin
Non-lexical lifetimes, which were just recently stabilized [1], make many of
the most common cases of borrow checker frustration "just work". And now that
the groundwork is done, even more cases will likely be supported in the
future.

[1] [https://rust-lang-nursery.github.io/edition-
guide/rust-2018/...](https://rust-lang-nursery.github.io/edition-
guide/rust-2018/ownership-and-lifetimes/non-lexical-lifetimes.html)

~~~
mehrdadn
Confused, how are these common use cases, and how is this "just working"? In
both examples on that page there is an unused variable (seems rare to do
intentionally?) and in the second case where one of them is used, it seems
they just elaborated the error message.

~~~
staticassertion
A common use case might be something along these lines:

[https://play.rust-
lang.org/?version=stable&mode=debug&editio...](https://play.rust-
lang.org/?version=stable&mode=debug&edition=2018&gist=7929c7ee98bf530888173d51a71f96a1)

While the `entry` API provided an ergonomic, more performant way to do this,
this is an example of a common pattern one might expect from other languages
that, until recently, did not compile.

NLL and other ergonomics improvements have helped a lot with these paper-cuts.

~~~
chrismorgan
Incidentally: for what that whole `match` block could be with the entry API:

    
    
      *example_map.entry("some_key").or_insert(0) += 1;
    

Or, if you prefer an extra intermediate for clarity:

    
    
      let entry = example_map.entry("some_key").or_insert(0);
      *entry += 1;
    

This is a good example of how, often, Rust’s approach of having greater
underlying complexity but exposing it sanely ends up allowing you to write
something that is semantically superior and easier to reason about, while
being faster too.

This and Rust’s ownership model (which I declare stands under the same banner)
is the sort of thing that I regularly miss when working in JavaScript (and
regularly missed when I used to work in Python plenty).

------
Aardwolf
In current C++, lots of std functions are annoying because you need to give
both .begin() and .end(), while in 99% of the cases when using a C++ container
you want to do your algorithm on the whole container (a function with 2
parameters is good to have for the other 1% of cases and the case of 2
pointers of course).

What took so long and why are "ranges" needed for that? Most other languages
have sort, transform, ... with a single parameter (such as a container or
array), and those don't require something called "ranges" for that.

Why couldn't C++ at least already have long had convenience functions that
just take a container and do the .begin() and .end() for you?

I'm all for having "std::transform" and stuff, but if it requires you to type
just as much as writing it manually with a for loop due to having to type the
name/expression of your container twice, what's the point.

EDIT: oh, and it's going to be std::ranges::sort(v) instead of just
std::sort(v)? What's wrong with naming it just std::sort(v)? Do C++ users
really need to be reminded it took a "ranges" concept just to sort a simple
container every single time they'll use it?

~~~
repsilat
I agree. The "flexibility" of being able to operate on a subset of the
container was never a good argument, because you could trivially make a
"view"/slice template type to fake begin/end to any pair of iterators.

Make the common case short and the uncommon case possible.

~~~
saagarjha
Ranges unify C arrays and C++ standard library types in a rather elegant,
general purpose way. Though, I’m still of the opinion that every function that
takes a range should also be overloaded to operate on the whole container as
well (and my personal C++ “library” does this already).

------
rsp1984
I know I'm not going to make a lot of friends with this opinion, but this is a
perfect example of everything that's wrong with modern C++.

Seriously who, except for a small circle of academics, actually believes that
this is good, readable, concise, maintainable code? Who _asked_ for this?

When I read

    
    
        inline constexpr auto for_each = []<
          Range R,
          Iterator I = iterator_t<R>,
          IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun)
            requires Range<indirect_result_t<Fun, I>> {
              return std::forward<R>(r)
              | view::transform(std::move(fun))
              | view::join;
           };
    

I can't help but being reminded of Java [1][2].

Meanwhile it's 2019 and C++ coders are still

\- Waiting for Cross-Platform standardized SIMD vector datatypes

\- Using nonstandard extensions, libraries or home-baked solutions to run
computations in parallel on many cores or on different processors than the CPU

\- Debugging cross-platform code using couts, cerrs and printfs

\- Forced to use boost for even quite elementary operations on std::strings.

Yes, some of these things are hard to fix and require collaboration among real
people and real companies. And yes, it's a lot easier to bury your head in the
soft academic sand and come up with some new interesting toy feature. It's
like the committee has given up.

Started coding C++ when I was 14 -- 20 years ago.

[1] [https://docs.spring.io/spring/docs/2.5.x/javadoc-
api/org/spr...](https://docs.spring.io/spring/docs/2.5.x/javadoc-
api/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.html)
[2] [http://projects.haykranen.nl/java/](http://projects.haykranen.nl/java/)

~~~
tomnj
This is library code which most users of the range library will not themselves
need to write. Using ranges absolutely leads to shorter and more elegant code.
The implementation of the library is complicated. Much of what’s likely
unfamiliar with this example is the use of C++ “concepts” (a technical term),
and is not actually necessary, but is placed there to actually give better
error messages to users of the library (without them, template code is
basically duck-typed, and can give terrible error messages). I’d argue that
the core of that code: the transform followed by the join is actually quite
readable.

I also wouldn’t at all say C++ is overly academic. It’s an extremely pragmatic
community. Yes, the standardization process is slow, but I don’t view, as a
C++ user, the issues you mention as serious. To me, there’s nothing wrong with
using high quality third party libraries like boost (and Eric’s range v3
library). My biggest frustrations with C++ are the build and packaging
stories.

~~~
Something1234
Boost is not high quality. It massively balloons the compile time, and there's
random incompatibilities between minor versions. It's also a hunt to figure
out which header I need to include for certain libraries. There's no
consistent feeling to the library. My biggest gripe is that it makes vims
completion stupid slow by bringing in a large number of headers.

~~~
fermienrico
You're talking about compile time and issues with your IDE. It is unfair to
call it "Not high quality". We can think about this from an end-user
standpoint that it bloats the IDE and impacts usability. Fine. Your
IDE/compile times may be different than others.

Boost is extremely high quality in terms of its documentation, algorithms, and
readability of the code - which is its core purpose. Please don't conflate
your minor development environment gripes with the excellence of Boost. It is
_really_ not fair.

~~~
jlarocco
I don't think it's an unfair complaint. Developers interface with the language
using compilers and IDEs, and until recently (with libclang getting more
popular) that interface has been terrible. In a lot of cases it's still
terrible. It's a real inconvenience, and there's no benefit coming with the
cost.

> Boost is extremely high quality in terms of its documentation, algorithms,
> and readability of the code - which is its core purpose.

I've used Boost for over a decade now, and IMO "extremely high quality" is a
stretch. Some modules are better than others, but there's no getting around
the fact that many of them are only necessary because the C++ standard library
is so bad.

~~~
fermienrico
I am having a hard time conciling compile times as a complaint. That is a
compiler problem. Or an inevitable problem as the project grows.

I don’t understand how that is a problem with Boost.

Same thing with the IDE. If you add an external library such as Boost, it is
natural that your intellisense or whatever IDE feature may get slower.

Both of these are undisputably an issue with the environment as a whole. How
on the earth is it a quality problem with Boost!? Please explain.

~~~
a_t48
Not OP but - Boost is a large library that can be tricky to extract only the
portions you want from, leading to instances where you compile a lot of
unwanted code, leading to ballooning compile times in exchange for only a
little bit of extra functionality that you want. In my experience boost
typically ends up being all or nothing, and very hard to remove once
integrated.

------
ddavis
Very excited to be able to use both ranges and concepts soon.

I started writing C++ just before the 2011 standard and it’s been fun to
follow and use the improvements over the last 8 years. I can’t imagine how
different the language will look (after the 2020 standard) to the eyes of
veteran C++ devs. It must already feel like a completely different language.

~~~
mark-r
Yes, it _does_ feel like a completely different language. The sample code in
the article is nearly unreadable to this C++ veteran. And based on the
verbosity of the result I'm not sure it's an improvement.

~~~
a1studmuffin
I have to agree. Pumping out C++ revisions every 3 years means I can no longer
assume a game written in C++ will port relatively painlessly from one
C++-compatible platform to another, as it all depends on which C++ standard is
supported by the platform SDK/toolset I'm forced to use.

------
piinbinary
Google cache:
[http://webcache.googleusercontent.com/search?client=ubuntu&c...](http://webcache.googleusercontent.com/search?client=ubuntu&channel=fs&q=cache%3Ahttp%3A%2F%2Fericniebler.com%2F2018%2F12%2F05%2Fstandard-
ranges%2F&ie=utf-8&oe=utf-8)

------
cjhanks
Is the C++ standards committee trying to make the language as incomprehensible
to its users as possible?

I cannot even begin to comprehend what assembly is going to be generated in
this code sample. Are those calls indirect? Can they be vectorized? How much
stack space is this going to use? What (if anything) gets put on the heap?

~~~
saagarjha
The C++ standard has nothing to do with these, and you won’t find a mention of
them at all in the text of the standard. The standard defines an abstract
virtual machine for running C++ code, and it lays out the guidelines for what
compilers are allowed to do with regards to the optimizations you mentioned.

~~~
repsilat
This is kinda true in theory, but about as far from the truth as you can be in
practice. Zero-overhead abstraction is a core part of the language's
philosophy, and that means features are designed to be translated to
"efficient" machine code. If nobody believed that optimising compilers could
wring something decent out of these features they wouldn't have been
standardised.

~~~
saagarjha
The abstract machine is designed to be relatively easy and efficient to
implement, but specific features such as vectorization are not part of the
standard. Remember, C++ runs on machines that don’t support any “advanced”
features, so there is a limit to what can be required in the standard.

~~~
cjhanks
I understand what you are saying, and I agree that it's true in principle.

But, if C++ did not provide me predictable optimizations - it would be
worthless to me. I know scientific computing is only one domain of computing.
But without this level of intuition for code -> asm. My day becomes filled
with reading objdump outputs, I don't want that.

------
lenkite
I think every HR reader just skimmed this article, saw that for_each example
and exploded in fury. He does say the below:

"I’m being a bit pedantic for didactic purposes here, so please don’t let that
trip you up.I also could have very easily written for_each as a vanilla
function template instead of making it an object initialized with a
constrained generic lambda."

He needs to clean up the article and write another which doesn't 'play around'
syntax quite so much.

~~~
pjmlp
It is C++, so it is going to be bashed not matter what, regardless that many
languages of similar age have their own set of quirks and breaking changes
even across minor versions.

------
GnarfGnarf
I've been writing C++ since 1995, and programming since 1965. I'm sorry but
this stuff is incomprehensible, opaque and cryptic. I don't see how it
improves productivity and maintainability. Although I do find the STL very
productive.

~~~
stinos
_I don 't see how it improves productivity and maintainability_

I find that quite hard to believe for someone with admittedly a lot of
experience. I mean, just look at the most basic example:

    
    
        func(vec.begin(), vec.end())
    

becomes

    
    
        func(vec)
    

Do you _really_ not see that can have advantages when it comes to productivity
and maintainability?

------
krackers
What's the difference between this and something like python-esque generators
which also appear to be lazy and have a much saner syntax? Doesn't C++ plan to
support coroutines which should provide a similar way to lazily evaluate
lists?

Also the example has a lot of boilerplate code that reminds me of template
metaprogramming hell. Is this something that's expected to be wrapped up
nicely by Boost & co. for end users?

~~~
mikeloomis
A c++20 range is just a bounded container. instead of having to write,
`(container.begin(), container.end())` for any function that uses iterators to
bound itself, you just write `(container)` and it knows its a range.

The range function in python is similar only in name, the c++20 range concept
is not a generator.

2\. There is a coroutine feature branch, I believe developed by Gor Nishanov,
[https://github.com/GorNishanov](https://github.com/GorNishanov). Also not
related to c++20 ranges.

3\. Eric Niebler used a single templated function. This is not anywhere close
to template metaprogramming hell, and I think you should look into templates
as once you learn about them they become way less intimidating. Also has
nothing to do with c++20 ranges.

~~~
amelius
I have yet to see a non-managed systems programming language which supports
coroutines properly.

Rust supports async/await, but it's not the same thing, as this comment
illustrates:

[https://news.ycombinator.com/item?id=15121199](https://news.ycombinator.com/item?id=15121199)

~~~
mikeloomis
It'll be after 20 for sure, we're still waiting for the executor TS for async
networking to be able to come in.

------
utopcell
Syntax should be dead-simple so that correctness is obvious, compilers don't
dump incomprehensible messages on trivial typos, and programmers can focus on
efficiency. For example, in something as simple as:

    
    
      int z;
      while(++z)
        for(int x = 1; x <= z; ++x)
          for(int y = x; y <= z; ++y)
            if(x*x + y*y == z*z) emit_triple(x, y, z);
    

a programmer would easily spot that x and y should never reach z, y should be
initialized to x+1 and that this will stop working as soon as z outgrows
16-bit values. A programmer would also see that this is a horribly inefficient
algorithm.

~~~
CamperBob2
Also, an electric shock should be emitted via the USB port whenever the
compiler detects that a programmer has written 'for', 'if', and 'while'
without a space, making them difficult to distinguish at a glance from
function calls.

~~~
AnimalMuppet
Function calls can be written with spaces, too...

~~~
mark-r
May you burn in hell forever if you code your for/if/while without spaces and
your function calls with spaces...

Remember that a compiler is not the only entity reading your code, other
humans have to read it too. Have pity on them, they're not as flexible as your
compiler.

------
slavik81
How does one try out these work in progress features? I tried compiling the
example on godbolt.org with gcc-trunk and wasn't sure how to get it working.

[https://godbolt.org/z/YSPXLu](https://godbolt.org/z/YSPXLu)

------
nindalf
Web Archive mirror -> [http://archive.is/lnrTw](http://archive.is/lnrTw)

------
azinman2
I feel like it should be called c::

------
alacombe
Having switched away from C++, I've got the feeling that this kind of code:

inline constexpr auto for_each = []<Range R, Iterator I = iterator_t<R>,
IndirectUnaryInvocable<I> Fun>(R&& r, Fun fun) requires
Range<indirect_result_t<Fun, I>> { return std::forward<R>(r) |
view::transform(std::move(fun)) | view::join; };

is a sort of magical incantation you spend 3 days to tweak, it essentially
just works, but becomes un-understandable within 2 weeks (even from the
author), un-reviewable and code that everybody is afraid to touch (or even get
close to) ?

Don't get me wrong, there is definitively a lot of flexibility I'm missing
from C++, even the problem can be addressed otherwise with other language, but
things can get very, if not utterly... convoluted.

~~~
htfy96
This should be a library function that will not be exposed directly to most
developers, who only need to know for_each(Range, fun) which is pretty
straightforward.

~~~
DoofusOfDeath
I've heard that line of reasoning before, and it doesn't make sense to me.

Specifically, I find it alarming that it assumes two tiers of competent c++
developers: a lower tier that just uses libraries, and a higher tier that can
write good libraries.

I see two problems with this view.

First, in my experience most large codebases are structured as many layes of
libraries. So the envisioned non-library developer may be in the minority.

Second, it's a red flag that the language's complexity dangerously high. If
nothing else, one of the envisioned non-library developers may unwittingly
write code that uses or misuses these features, and _still_ need to debug
incomprehensible compilation- or runtime-errors.

~~~
saagarjha
> If nothing else, one of the envisioned non-library developers may
> unwittingly write code that uses or misuses these features, and still need
> to debug incomprehensible compilation- or runtime-errors.

The reason that the code is this complicated is so that end users don’t shoot
themselves in the foot (performance-wise, correctness-wise) when using these
features. And error messages will get better when concepts land.

To address your main point, though: there is always a separation between
library authors and library consumers, even if they are written by the same
people, because the requirements are different in each case. A library author
must write general and useful code, while a library user can “get away with”
hard coding logic and customizing uses to only cover what they need. Hence,
the job of a library author is usually much harder.

------
mimi89999
I only see a database error on this website.

~~~
eric_niebler
I just upgraded my GCE instance. Let's see if it can withstand this withering
assault. <makes popcorn>

------
fourthark
Congratulations Eric! Awesome work.

------
MichaelMoser123
my biggest concern is that Scott Meyers will not be around to explain all the
new revelations (like concepts) to us mere mortals - he says he was done with
C++.

------
laythea
I think the C++ committee has gone a step or few too far. This code is
horribly ugly.

------
aaaaaaaaaab
Why would anyone invest in this? I thought C++ was basically in maintenance
mode given that greenfield software tends to be written in Rust...

~~~
war1025
C++ is extremely widely used. Especially since C++11, the language has
received a ton of attention for making things more ergonomic. As projects
update compilers, they are able to take advantage of these updates. C++ is
anything but in maintenance mode.

Rust fits in a similar niche, but it has a much smaller following. Several of
it's main contributors just happen to be HN regulars, so it gets more
attention than it otherwise would.

~~~
aaaaaaaaaab
By the amount of downvotes I got you seem to be right.

I was genuinely under the impression that C/C++ was being progressively phased
out in favor of Rust, but looks like this is hardly the case. I guess I should
probably peek outside the HN filter bubble more often...

~~~
shakna
C++ has had major specification updates (some are smaller than others) in
2011, 2014, 2017, with a planned one in 2020. This pattern will be ongoing for
the foreseeable future.

These specification changes are usually implemented by compilers _before_ the
final release date. Some features take longer, but you're usually only look at
6-12 months before being able to use 100% of cutting edge.

If anything... C++ is ramping up.

