as someone who knows C++, but doesn't use it everyday, nor keep up with the latest and greatest in the community, I was surprised to see the "pipe" syntax. It looks like the author is using them like unix pipes, or like the `|>` macro in Elixir. When did C++ get this? Is it just functions overloading the bitwise-or operator, or is it 'official' syntax?
What reason could we have for making `read_ints` and `unique` eager by default?
Performance-wise, the lazy filter works in constant space, will result in fewer dcache misses if the data set is large, and (I think) provides more opportunity for the optimizer to do clever things. The eager chain may have better icache performance if the filter is very complicated.
And it's easy to go from lazy to eager -- just put everything in a vector. If you want to go from eager to lazy, though, you're out of luck.
I think it comes down to a "memory bug" vs. "performance bug" trade-off. If you make it default-lazy you run the risk of having the stream evaluated multiple times. If you make it default-eager you run the risk of O(N) memory (as you said).
I agree with your side of the trade-off, a performance bug (result: slow application) is always preferable to a memory bug (result: failed malloc, likely crash) in production.
To make things more desirable, you could always do something even simpler than your example:
auto ints = read_ints() | cont::sort | cont::unique | cont:to_vector;
That way fixing the performance bug would only need a one-liner.
In my experience, it's really important to be able to look at some code and know its performance characteristics: does it allocate memory? Who is managing the lifetime of the objects and how many times are the elements copied/moved? How many passes over the data are made? The separation of eager from lazy adaptors makes it possible to know those performance characteristics at a glance.
If `cont::unique` is lazy (non-owning), what is the lifetime of the elements allocated by `cont::sort` (or moved from `read_ints()`)? If the code had been written instead like below, who is responsible for keeping the elements alive?
It still surprises me to this day how much minutia is found in C++'s standard libraries when it comes to data structures. I have a hard time imagining how one could write a more full-featured set of containers and the like.
For all of the STL's power, and all the useful stuff in std::algorithm, it's still impossible to write something as elegant as a list comprehension in Python. It's really exciting to see Eric's work taking us closer to this goal.
On the other hand, I always worry about all the magic implicit behavior in C++, both in old and new language features. We're writing in C++, we care about efficiency. Eric addresses this by requiring rvalue references in the container algorithms, but in general it's pretty easy to write code that does more work than you expected.
Since most undesired magic in C++ boils down to allocating heap memory unexpectedly, it would be cool to add syntax to could tell the compiler that a block is not allowed to allocate, like:
12 comments
[ 2.6 ms ] story [ 37.2 ms ] threadThe first mayor library to use this syntax was Boost.Range and it is now being proposed as the syntax of standard ranges.
> Eric Niebler contributed the Range Adaptor idea which is arguably the single biggest innovation in this library.
Why can't the
from TFA can't be done "as lazily as possible" (i.e., sort eagerly and uniq lazily). Alternatively, something like or even What reason could we have for making `read_ints` and `unique` eager by default?Performance-wise, the lazy filter works in constant space, will result in fewer dcache misses if the data set is large, and (I think) provides more opportunity for the optimizer to do clever things. The eager chain may have better icache performance if the filter is very complicated.
And it's easy to go from lazy to eager -- just put everything in a vector. If you want to go from eager to lazy, though, you're out of luck.
I agree with your side of the trade-off, a performance bug (result: slow application) is always preferable to a memory bug (result: failed malloc, likely crash) in production.
To make things more desirable, you could always do something even simpler than your example:
That way fixing the performance bug would only need a one-liner.Still, having the option is better than nothing.
Looking at your code:
If `cont::unique` is lazy (non-owning), what is the lifetime of the elements allocated by `cont::sort` (or moved from `read_ints()`)? If the code had been written instead like below, who is responsible for keeping the elements alive? There's a whole host of issues here that you're not dealing with.- flat set/map - multi_index - static_vector (called SmallVector in LLVM) - bimap - intrusive containers - various from Boost.Heap
On the other hand, I always worry about all the magic implicit behavior in C++, both in old and new language features. We're writing in C++, we care about efficiency. Eric addresses this by requiring rvalue references in the container algorithms, but in general it's pretty easy to write code that does more work than you expected.
Since most undesired magic in C++ boils down to allocating heap memory unexpectedly, it would be cool to add syntax to could tell the compiler that a block is not allowed to allocate, like: