C++20 Ranges
ericniebler.com
ericniebler.com
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)
}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.
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.
[1] http://softwaremaniacs.org/blog/2016/02/12/ownership-borrowi...
In the general case that's theoretically impossible, and also not really a good measure anyway.
[1] https://rust-lang-nursery.github.io/edition-guide/rust-2018/...
https://play.rust-lang.org/?version=stable&mode=debug&editio...
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.
*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).
The Pin<> type constructor is being standardized to support this case. This is not so much "custom move semantics" as a lack of in-built "move" semantics of any sorts, but the basic use case is being worked on.
> And does the compiler no longer complain about code that really should work just fine?
As other people have mentioned, the NLL feature addresses many cases where this used to be a problem, and has now been introduced to the 'stable' version.
In Python it's blindingly obvious what is happening: one is creating a group of 3 values, where the values are in specific ranges if a certain condition is met. Itertools.count() is perhaps opaque, but otherwise this is literally how the code reads!
Rust is already going into the weeds after the first '=' with its flat_maps and (1..) = c, whatever the heck that means and the C++ code is not even worth talking about.
Performance is not an excuse, since loops can solve this with good performance and probably the best readability.
The 1..=c is Rust syntax for an inclusive range. Rust also has syntax for ranges that exclude the last value like Python’s range(1, c + 1), which is 1..(c + 1). Using that would make it look more like the other examples, though since the language has syntax for the idea “1 to c inclusive”, it makes sense to just use it.
For comparison Ruby uses a..z and a...z for inclusive and exclusive ranges respectively. I like that Rust’s inclusive range is more visually distinct, and I can see why they went for a symbol that implies the last element “equals” another value, but it does look unusual at first glance.
In P6, 1..10 is exactly the range you'd expect, while ^ is used to exclude either end of the range. For example, 1..^11 is the same as 1..10, as is 0^..^11 (can't say I've ever needed that, but I could see a couple of possible uses).
This also carries over into range syntactic sugar. ^10 is shorthand for 0..^10, and as a result ^@list.elems is a list of valid indices of a list.
I personally find Rust's syntax noisier, but it's a small issue.
That syntax is impressive, handles all four possibilities nicely.
However since reading this short article by Dijkstra, I’ve been convinced that 0..N ranges (inclusive start, exclusive end) are the way to go whenever there’s a choice:
http://www.cs.utexas.edu/users/EWD/ewd08xx/EWD831.PDF
There's certainly a mental benefit of having made a choice and being consistent, which I think is possible with any of the options, but I find the argument of having length = end - start compelling.
It does fall down in some cases, eg when looking for a range over all unsigned ints, as the total number of uints isn’t a uint itself (as uints start at zero), so some way to represent a rage that is inclusive of both ends is still needed.
triples = [(a,b,c) | c <- [1..], a <- [1..c], b <- [a..c], a*a + b*b == c*c]
mapM_ print $ take 10 triples auto for_each = [](R&& r, Fun fun) {
return r | view::transform(fun) | view::join;
};
similarly, it seems that yield_if can also be radically simplified auto yield_if = []<Semiregular T>(bool b, T x) {
return b ? view::single(x)
: view::empty<T>;
};
But if you actually read the blog post... for_each and yield_if are a part of the range_v3 library and are proposed additions to the language.Also I don't know what the heck is up with that cout statement; we can use structured declarations to make that look more pythonic too. And there's also a proposal for string formatting [1]
#include "fmt/format.h"
#include <iostream>
#include <ranges>
using namespace std;
using namespace view;
using namespace fmt;
int main() {
auto triples =
for_each(iota(1), [](int z) {
return for_each(iota(1, z+1), [=](int x) {
return for_each(iota(x, z+1), [=](int y) {
return yield_if(x*x + y*y == z*z,
make_tuple(x, y, z));
});
});
});
for(auto [x,y,z] : triples | take(10))
cout << format("{}, {}, {}", a, b, c) << endl;
}
So despite all the protests about how ugly Eric's solution is... I don't think the above is significantly worse than your python & rust equivalents. Speaking as a python evangelist who uses C++ out of nece$$ity.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?
Make the common case short and the uncommon case possible.
In C++, that was not trivial at all. Implementing a custom container with iterators (which you'd have to do for this - you can't just propagate iterators as is) is very verbose, since there's no syntactic sugar for it, like say generator functions in Python.
They aren't, but without ranges, things that accept iterators are more general than things that accept only containers (e.g. you can't iterate over half a container without a range, iterators, or copying to another container through other means defeating which would defeat the whole point.)
As such it makes sense to have a generic range type first, then algorithms that operate on containers/ranges.
Additionally, if you can take a range<Iter> instead of a Container (I haven't even checked if C++20 algorithms do), you might get better error messages (e.g. "Xyz cannot be converted to a range" instead of "Xyz doesn't implement begin()").
> What took so long
Everything takes long in C++ standards land. I have no special insight into the actual workings of the standards committee, but I've always chalked it up to a combination of being extremely conservative about changes, made worse by past missteps burning them, and less than ideal foundations which already made C++ hard to parse, slow to compile, and inconsistent across compilers, and often not fully implemented.
It's not like there's been some giant technical hurdle across the board - boost::range has been around for well over a decade (in boost since 1.32.0 in 2004).
Faster iterating languages usually have a fresh start unencumbered by backwards compatibility concerns with large existing codebases, less cruft inherited from other languages (cough C cough), and a single official implementation which can be used to more quickly prove out good ideas and discard bad ones.
Yes but they could have provided both a single-container-parameter version and a two-iterator-parameter version of all those functions like sort and transform. Templates and SFINAE support that, and the single parameter one would call the two parameter one with .begin() and .end() for you.
They should try to make the std library such that you type shorter code, not longer code, imho: longer expressions are less readable. But well, it looks like ranges mostly do that for the user side (except that it uses std::ranges:: instead of just std:: as prefix) so at least that's nice.
For my own libraries I'd skip the iterator accepting version. Implementing one range type is way easier / less prone to oversights and other mistakes than double implementing every algorithm. Or more than double implementing, if you need perfect forwarding for non-copyable functors without std::forward pre-C++11. Even though all they do is forward.
SC++L doesn't get much choice since they started with iterators only, but even there, I'm not sure how much time skipping std::range saves you. You can't even take a plain C style array without either a range type, free-form std::begin/std::end, or the iterator variant.
Doesn't work with arrays, since they don't have member functions.
You might find this intriguing https://github.com/LoopPerfect/conduit
An unbounded enumerable would be represented by some way to obtain a forward-only iterator - also an existing C++ idiom.
(a) C++ is an international standard, has a huge installed base of business- and mission-critical software, probably billions of lines of code, tens of millions of programmers, and we can't afford to get things wrong (though we still do sometimes).
(b) Nobody has built a large, _very_ generic, general purpose, high-performance library, on top of the (also new) C++ concepts language feature before. We were figuring it out as we went.
(c) While standardizing this piece, we were also thinking 5 to 10 years down the road and trying to make sure what we were standardizing now would evolve well. For instance, we already know pretty well what using ranges with coroutines will look like (i.e., reactive streams). We already know what other lazy range adaptors we want, and we know they work with the bits we're getting for C++20. We know how to integrate eager adaptors that transform containers and compose. This all needed to be built, tested, and well-understood.
As for why it is `std::ranges::sort` instead of `std::sort`, this exercise wasn't just about adding overloads that take ranges instead of iterators. It was also about rigorously specifying in code what the requirements of each algorithm are. For Reasons, we can't just slap new requirements on old algorithms that have been shipping for 20 years. It will break mountains of code. Nobody likes having two `sort` algorithms in two different namespaces. It is preferable to the alternative.
If you don't want to deal with begin/end, there are lots of utility libraries, and there's not much reason to standardize it. For example, here is Google's wrapper library (disclosure: my employer) that takes "just the container": https://github.com/abseil/abseil-cpp/blob/master/absl/algori...
Regarding your edit: it's pretty simple in c++ to alias namespaces as needed for your project. The standards committee is working with a big install base, and they have a few hard requirements. They don't break the ABI. They try not to break calling code in other ways. (Adding overloads while client code passes around pointers to your functions can have surprising effects - perhaps causing their code to stop compiling? Pretty sure one of abseil's requests is that clients don't take addresses of their functions for this reason
Stack overflow on taking addresses of overloaded functions: https://stackoverflow.com/questions/2942426/how-do-i-specify...
Abseil compat guidelines: https://abseil.io/about/compatibility)
It's just a boost header that you can use which does exactly this : https://www.boost.org/doc/libs/1_69_0/libs/range/doc/html/in...
It's your problem if you haven't been using it.
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... [2] http://projects.haykranen.nl/java/
The constexpr story is presumably very similar to what happened with templates. There was a genuine niche use case, and that was perfectly covered in the initial implementation. But the feature just lends itself perfectly to extremely clever, single A4 page example code. No one ever went "ooh" and "aah" at a basic_string::split paper.
When my university peers and I were learning C++, a lot of us strongly preferred using C99 since it didn’t seem as awful in terms of complexity and verbosity.
auto for_each = [](R&& r, Fun fun) {
return r | view::transform(fun) | view::join;
};
I also removed inline and constexpr because he didn't need them in his example.I don't disagree that it's ugly, but it's not as bad as it looks when you go through the standard library. Those are functions that have been written, optimized and generalized to handle all sorts of cases. Your needs are almost certainly more specific, and you will undoubtedly write functions that are much simpler.
That being said, I kind of agree. I would have preferred your list of features to getting Concepts. I find SIMD to be a particular sore spot.
Julia is the only modern language with beautiful syntax. I think we should pay more attention to the aesthetics of a language just like we do with spoken languages. Certain languages just sound beautiful - Japanese and French.
C++ is old but Rust could have paid more attention to subjective aspects of a language.
You've managed to pick the two languages whose sound irritates me the most, which I think illustrates the fragility of this kind of thinking perfectly.
Whether :: specifically is the best choice of such an operator is another question. But there aren't exactly many options left, and this one is firmly established by now, even outside of C++.
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.
But...I will say some good ideas made it into the standard thanks to a first implementation in boost, though what ended up being standardized is typically much cleaner and more orthogonal.
Then again this is how we got the standard containers and so much more standard library goodness -- via Stepanov stepping up and writing the STL.
The best way to think of boost as the npm.
This is just false.
Well that's because it is really a collection of mostly unrelated libraries by a large number of different authors.
There is some general theme (emphasis on value based generic programming in the stl style as opposed to more traditional Oop), but that's about it.
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.
But so is the viewpoint that one of the major problems with C++ culture is the misconception that developer experience and high quality tooling are afterthoughts and orthogonal to "quality". In this viewpoint, readability in a editor or IDE at least as important as readability in a browser or email.
Complaining about an external third party library that is trying to fix the issues for its compile time and how fast it loads IDE is very obtuse and unfair. Boost is a bandaid. Don't complain about the bandaid, complain about what causes the wound. Overall, I agree with you that C++ has poor user-experience - don't blame Boost for it!
> 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.
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.
It is entirely dependent on lazynes, first class functions and complex compiler optimizations. But there isn't much specific code for comprehensions there.
> and is not actually necessary, but is placed there to actually give better error messages to users of the library
That's not the first time I see this kind of reasoning; you can find it all over programming. If the alternative is an API that sucks (and yes, template errors in C++ do suck), picking that alternative is not a very viable option.
Another scenario where this often pops up is whenever someone complains about very verbose error handling. If you want to write quality software, you shouldn't just skip that part (in fact I'd say it's the most important part of the code you're writing). In the same vein, there's a talk by Andrei Alexandrescu somewhere (on D), where he points out that probably 0% of the main functions ever written in C or C++ are correct in that they catch everything that can go wrong.
Also, since I find that most code should be written in a library style (i.e reusable, well-encapsulated) except if it's explicitly tied to a specific program (say config or cli argument parsing), I also don't find "only library implementers will have to deal with this" is a convincing argument. OTOH, at least, writing in a library style doesn't necessarily imply overly generic code, so that might not be that much of an issue.
As for std::string I never find myself using boost for any reason so I'm curious what the indispensable boost feature is for strings. The only thing I want from std::string is the ability to resize it without initializing allocated space, the ability to construct a string from an existing data pointer, and the ability to release the data pointer from an existing string (basically the union of a string a unique_ptr).
Edit: also wanted to add - what a disaster
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.
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?
Most companies are still busy dealing with their C++98 backlog libraries. I don't expect this to get in production code within 5 to 10 years...
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.
Since then, both the library and compilers have improved.
"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.
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?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?
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. 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.
Rust supports async/await, but it's not the same thing, as this comment illustrates:
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.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.
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.
Doesn't really matter at the end. I'm fairly certain the code in question was canned later on by more "old school" devs...
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.
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.
> Error establishing a database connection
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.
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...
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.
However I will keep writing C++ to go along my Java, .NET, nodejs production code, for the time being.
Rust is still lacking in IDE support, mixed language debugging, OS SDKs I generally use, integration with binary libraries, not everyone on the teams is willing to add yet another language to the mix, NVIDIA GPUs are designed for C++, and newer features make it easier to write safer code, in spite of its C underpinnings.
Yes it looks phylosophically nice, but out of the language purist&geek, managers will not care if you are allowed or not to shoot you in the foot, they'll only care if you can deliver or not in due time.
C++ is not memory safe - which is one reason why e.g. Java and C# are even more "extremely widely used" than C++. Rust is memory safe, and has C++ -like performance (and arguably better than C++, with e.g. its restriction of pointer aliasing and its high-level support for things like simple parallel programming).
Not to mention the long history of C++. Rust has a very long way to go to catch up.
You are terribly mistaken.
> given that greenfield software tends to be written in Rust...
You are terribly mistaken again.
True, but I sure hope that changes sooner than later. The top comment here echoes my sentiment about what C++ has become, while another one illustrates how cleanly the example can be done in Python and Rust. The problem as I see it is that long time C++ developers have learned all this arcane BS right along with its development. Anyone new to a language will vomit when they try C++ compared to Python or Rust. The C++ language developers are probably the least able to see what the problem is in this regard.
On a slight tangent, IMHO this approach to language development is at the heart of the issue that lead Guido to step down as Python lead. One of the proposals that got accepted crossed the line between readable code and "this is a compact way to do this but isn't intuitive on its face".
Nobody uses Rust in production. Why such thoughts in general?
(Still way less than C++, of course. But we’re making big strides!)