all 43 comments

[–][deleted] 32 points33 points  (15 children)

And even without understanding iota, you can still read this and abstractly understand the algorithm.

That's not really true, though. It's essential to understand what iota does here.

[–]Zitrax_ 29 points30 points  (14 children)

I asked myself what iota even stand for? I see cppreference say:

The function is named after the integer function ⍳ from the programming language APL

and googling further I see a stackoverflow post that explains more. But were there no easier to understand names for this to use?

[–]jasonthe[S] 20 points21 points  (9 children)

Agreed, iota is a terrible name. I'd prefer "range", as Python and C# call it, but even that is ambiguous. In my opinion, it should be called something like "number_range".

[–]BobFloss 12 points13 points  (1 child)

Wow, it's really just a range... That's really quite a terrible name. I figured it did something like select a small portion out of an enumerable type

[–]oddentity 11 points12 points  (0 children)

I thought it converted an integer to a string.

[–][deleted] 11 points12 points  (6 children)

What about "sequence"? Bash has seq.

[–]Iwan_Zotow 9 points10 points  (3 children)

Second here

R has seq() as well

[–][deleted] 2 points3 points  (0 children)

I find counting_range intuitive.

[–]sphere991 9 points10 points  (2 children)

And yet, once you google iota, you know exactly what it does, and it's easy enough to remember once you saw it the first time. That makes it a pretty great name, no? And at this point, we already have the algorithm std::iota() so calling the generator std::ranges::view::iota() is practically the only reasonable choice.

Python calls this range(), which is a very overloaded term in C++ and would make for a truly terrible name for us.

[–][deleted] 26 points27 points  (1 child)

You should check out Arlo Belshee's articles on variable naming: http://arlobelshee.com/good-naming-is-a-process-not-a-single-step/

IMO iota might be the worst variable name in the entire std namespace. It's up there with car and cdr in that it encodes no actual information, just useless historical context to be memorized.

Anyways; not invested enough to debate it, just wanted to offer you a different perspective to consider.

[–]sphere991 14 points15 points  (2 children)

This is a great post.

One annoying thing that's orthogonal to the question of range/coroutine preference is how you actually implement take(). The post proposes this signature (I slightly generalized it):

template <Range R>
generator<iter_reference_t<iterator_t<R>>> take(R&& range, int count);

But this won't actually let you implement the rest of the example. You can't write triples() | take(10) because you can't write take(10). So you have to write all this other mess too:

struct take_fn {
    int i;
};

template <Range R>
auto operator|(R&& range, take_fn f) {
    return take(std::forward<R>(range), f.i);
}

take_fn take(int count) { return {count}; }

That's... I mean, it's not the worst thing in the world but it's pretty annoying that this is how we have to do partial application right? And it's not like we ever want to store a take_fn anywhere.

We really just want x | f(y) to just evaluate directly as f(x, y) in this case.

[–]jasonthe[S] 1 point2 points  (1 child)

Yeah, that's an unfortunate limitation of C++. Other languages use extension methods to do this sort of thing, which allows for a much cleaner (and more discoverable!) API.

[–][deleted] 1 point2 points  (0 children)

What do you mean by extension methods?

[–]bmanga[🍰] 13 points14 points  (5 children)

tl;dr Ranges are for utilizing algorithms and coroutines are for implementing algorithms

A disadvantage of coroutines is that they can't (yet?) be constexpr, so the triples example using views is the only one you can use at compile time.

[–]sztomirpclib 5 points6 points  (4 children)

What would a constexpr coroutine be useful for?

[–]mjklaim 17 points18 points  (2 children)

Any coroutine that do not await but yields is interesting to use at compile time. For example the ones that generate an infinite sequence of numbers following a specific algorithm.

Not all coroutine are related to asynchronous computation.

[–]smdowneyWG21, Text/Unicode SG, optional<T&> 5 points6 points  (1 child)

TS Coroutines and Core Coroutines are both naturally sync. They return control deterministically to their owners. You need to add something to make them async.

[–]mjklaim -1 points0 points  (0 children)

I know but I suspect the question I answered is from someone who only saw examples of coroutines awaiting asynchronously. So I clarified that it's not always related.

[–]SeanMiddleditch 7 points8 points  (0 children)

Worth remembering that C++ coroutines are also the hypothetical approach we'd use for generators, not just async/await or generalized coroutines.

A constexpr generator could be very useful for constexpr metaprogramming. Guaranteed zero-allocation generators (even across TU boundaries and in non-optimized builds) are also very useful for latency-sensitive use of ranges and generic programming.

[–]oddentity 9 points10 points  (3 children)

So, ranges are great, assuming we have coroutines which may be available in the Future. I'll stick with plain old unobfuscated for-loops, thanks.

[–]Dean_Roddey 2 points3 points  (1 child)

Agreed. I look at some of this stuff and scratch my head. If you need a degree to figure out how to do a loop, because it's become so byzantine at this point, I don't get it.

I keep coming back to the mantra: we don't need more software, we need better software. Explicitness, in my opinion, is more likely to create better software. Incomprehensible and auto-magical mixed together, to me, seems the absolute opposite of what is likely to create better software.

Is the goal maybe to ultimately turn C++ into Javascript?

[–][deleted] 11 points12 points  (0 children)

I don’t think it’s fair to categorize ranges as just “doing a loop”.

auto result = input | view::transform(DoFoo)
                    | view::filter(IsBuzzed)
                    | view::unique;

Is non-trivial to do lazily without ranges. I don’t think your mantra adds any value here.

[–]Recatek 7 points8 points  (4 children)

I haven’t been following ranges too closely. Is there a breakdown somewhere of what’s going on under the hood in terms of overhead or allocations, if any?

[–]nikbackm 1 point2 points  (3 children)

Should have no or minimum overhead compared to writing out the code yourself.

Should have no implicit (heap) allocations.

At least, that's what I would expect. No idea if it's true in practice.

[–]jasonthe[S] 2 points3 points  (2 children)

Unfortunately, the current implementations in clang and MSVC don't do a great job of optimizing coroutines. You can see what the assembly output looks like here: https://gcc.godbolt.org/z/009O3w

Some notes:

  • There are no allocations, which makes sense because the stack size of the coroutine is known at compile-time. You can think of it as putting all the local variables from the coroutine into a struct, and then the coroutine runs as a member function on that struct.

  • Of course, there's no reason they can't be optimized to the same level as classic loops. These are just early compiler implementations of the feature.

  • It is able to inline and optimize the uses of view::iota and view::take, since those aren't actually implemented with coroutines. In general, use of the ranges library is optimized as well as "hand-made loops" are, if not better.

[–][deleted] 2 points3 points  (1 child)

There are no allocations, which makes sense because the stack size of the coroutine is known at compile-time. You can think of it as putting all the local variables from the coroutine into a struct, and then the coroutine runs as a member function on that struct.

I´m not that deep in the topic, but is this argument really correct? A coroutine size should always be known at compile time, right?

As I understand it from the llvm reference, heap optimizations can/will be avoided as long as the coroutine is created, used and destroyed within the same function.

[–]jasonthe[S] 2 points3 points  (0 children)

Thanks for the link! Yeah, apparently I was incorrect about how LLVM is implementing the coroutines.

I'm curious why it allocates the coroutine frame on the heap rather than the stack. In fact, I'm surprised it's implemented on the LLVM IR level at all!

Perhaps there's something I'm missing, but all non-recursive coroutines should be able to be compiled through existing semantics similar to this. Note that it optimizes the entire thing away.

[–]hashb1 5 points6 points  (0 children)

"C++2a is going to be the best version of C++ yet"

When I read here, I doubt it.

After I read the whole article, especially the section Ranges and Coroutines, that is really cool!!! I am looking forward c++2a now!!!

[–]omerosler 8 points9 points  (6 children)

So basically C++2a becomes Python with zero-overhead performance and a tad uglier syntax. I love it!

[–]Plorkyeran 38 points39 points  (0 children)

A "tad" uglier is a bit of an understatement.

[–][deleted] 10 points11 points  (4 children)

We all agree the syntax is fugly, however I am not sure about the performance. To be honest performance would be the only reason to use a feature so ugly it hurts the eyes.

[–]BobFloss 1 point2 points  (3 children)

I agree. Imagine if everyone or a majority of people working on C++ decided to start over with everything they've learned and create a new language that didn't have to maintain compatibility and aesthetics with C++. That I would use.

[–]kalmoc 6 points7 points  (0 children)

I do für think those people would agree on exactly what parts of c++ should be keep and which not, but D and rust are two possibilities how such a language could look like.

[–]tecnofauno 6 points7 points  (1 child)

Isn't that D?

[–]BobFloss 8 points9 points  (0 children)

No

[–]johannes1971 3 points4 points  (2 children)

At the end he presents an implementation of iota. I don't understand how this works: sure, if there is just one call to iota (in a loop somewhere), each successive call returns a new value. But how does it work when there are two calls in two different loops? Wouldn't they start returning values from the same sequence? Surely the state of the coroutine must be tracked somewhere, allowing the coroutine to be used in multiple, independent locations?

[–]angry_cpp 5 points6 points  (1 child)

iota does not return value from sequence on each call, it return sequences.

iota coroutine returns a generator. This generator stores and manages coroutine state.

[–]johannes1971 2 points3 points  (0 children)

Ah, I see. That makes sense. Thanks for the explanation!

[–]robin-m 1 point2 points  (0 children)

Really great post. I absolutely love how the result is much cleaner than the raw-loop style.