C++ Weekly - Ep 427 - Simple Generators Without Coroutines

Sdílet
Vložit
  • čas přidán 5. 05. 2024
  • ☟☟ Awesome T-Shirts! Sponsors! Books! ☟☟
    Upcoming Workshop: Understanding Object Lifetime, C++ On Sea, July 2, 2024
    ► cpponsea.uk/2024/sessions/und...
    Upcoming Workshop: C++ Best Practices, NDC TechTown, Sept 9-10, 2024
    ► ndctechtown.com/workshops/c-b...
    Episode details: github.com/lefticus/cpp_weekl...
    T-SHIRTS AVAILABLE!
    ► The best C++ T-Shirts anywhere! my-store-d16a2f.creator-sprin...
    WANT MORE JASON?
    ► My Training Classes: emptycrate.com/training.html
    ► Follow me on twitter: / lefticus
    SUPPORT THE CHANNEL
    ► Patreon: / lefticus
    ► Github Sponsors: github.com/sponsors/lefticus
    ► Paypal Donation: www.paypal.com/donate/?hosted...
    GET INVOLVED
    ► Video Idea List: github.com/lefticus/cpp_weekl...
    JASON'S BOOKS
    ► C++23 Best Practices
    Leanpub Ebook: leanpub.com/cpp23_best_practi...
    ► C++ Best Practices
    Amazon Paperback: amzn.to/3wpAU3Z
    Leanpub Ebook: leanpub.com/cppbestpractices
    JASON'S PUZZLE BOOKS
    ► Object Lifetime Puzzlers Book 1
    Amazon Paperback: amzn.to/3g6Ervj
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Object Lifetime Puzzlers Book 2
    Amazon Paperback: amzn.to/3whdUDU
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Object Lifetime Puzzlers Book 3
    Leanpub Ebook: leanpub.com/objectlifetimepuz...
    ► Copy and Reference Puzzlers Book 1
    Amazon Paperback: amzn.to/3g7ZVb9
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► Copy and Reference Puzzlers Book 2
    Amazon Paperback: amzn.to/3X1LOIx
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► Copy and Reference Puzzlers Book 3
    Leanpub Ebook: leanpub.com/copyandreferencep...
    ► OpCode Puzzlers Book 1
    Amazon Paperback: amzn.to/3KCNJg6
    Leanpub Ebook: leanpub.com/opcodepuzzlers_book1
    RECOMMENDED BOOKS
    ► Bjarne Stroustrup's A Tour of C++ (now with C++20/23!): amzn.to/3X4Wypr
    AWESOME PROJECTS
    ► The C++ Starter Project - Gets you started with Best Practices Quickly - github.com/cpp-best-practices...
    ► C++ Best Practices Forkable Coding Standards - github.com/cpp-best-practices...
    O'Reilly VIDEOS
    ► Inheritance and Polymorphism in C++ - www.oreilly.com/library/view/...
    ► Learning C++ Best Practices - www.oreilly.com/library/view/...
  • Věda a technologie

Komentáře • 49

  • @markus1063
    @markus1063 Před 27 dny +13

    just realized that in Python there has been a lot of interest in adding type annotations for readability purposes, while C++ has been adding support for writing code with less type annotations for the same reason.

    • @keris3920
      @keris3920 Před 27 dny +1

      I started inverting the argument about "readability" lately, and instead focus on things that are "terrible" to read. I don't think there is a perfectly readable way to write code, and any attempts at achieving that will probably yield contradictory results such as what you're suggesting.
      Another one, is I see a lot of C++ developers saying they hate trailing return type syntax because it's unreadable. And yet, Python folks would love to beg to differ lol.

    • @shailist
      @shailist Před 27 dny +4

      python is fully dynamic, and types aren't enforced or even recognized by the language, but 99% of the time you know what the type of a variable will be. that is the main argument for type annotations in python, it doesn't change the behavior of the code, but makes most variable declarations more clear. note that you'll almost never see code fully type annotated. if the type annotations don't add to the readability of the code, the type could easily be inferred from the value, or the type can't be known, you won't type annotate a variable.
      in C++ it is kind of the opposite. there is no dynamic typing. everything has a static type associated with it, but in many cases specifying the type makes the code *less* readable. the classic example is container iterators. that's why C++ introduced the auto keyword - the type *must* be deducible, but you save on the visual clutter of specifying the entire type.
      the case for trailing types (and return types) is consistency. types can sometimes be very long, but a name is always a single identifier. imagine you had to scroll just to see the name of the function - not very fun

    • @spastukhov
      @spastukhov Před 27 dny +1

      Type annotations in Python is not just for readability, I'd personally value more the ability to validate match of expected type to actual type (wile runtime, unfortunately), otherwise a comment would be just enough. It's just piece of type safety to purely unsafe nature (in type safety area) of python itself.
      C++ remains type safe even with `auto`, it just infers the type, so you put it there in the code to say "I don't know exact type of the thing" (better to combine with type requirements then) or "I don't want to put messy type definition here" (like for iterators) or "I don't care what type is".
      So, I personally would not put C++'s tools to not writing type into the same domain of readability with python's type annotations, the main purpose of the tools is completely different.

    • @markus1063
      @markus1063 Před 27 dny

      @@spastukhov True, there are other benefits from python's annotations. I now realized that I articulated my original point in a way that could be understood that readability is the only reason 🙃

  • @avramlevitter6150
    @avramlevitter6150 Před 28 dny +15

    The generator could wrap the function and do the discarding for you instead of having your function discard it.
    What I was expecting was a small dive into writing your own ranges, would be nice for a future episode.
    By the way, thiis ends up having a bit of an interesting phenomenon where dereferencing the begin of the resulting view advances the sequence.

  • @dholmes215
    @dholmes215 Před 28 dny +5

    The Range-v3 library that std ranges were based on has a generate_view range to do this that didn't make it into the standard. Its implementation is more complicated; I wonder how its performance compares to this.

  • @XzatonJW
    @XzatonJW Před 26 dny +1

    You could check if the invocable is callable with the iota output and if not wrap the callable in another lambda

  • @gregorssamsa
    @gregorssamsa Před 27 dny +1

    It’s unintuitive to many that this would generate such clean code. Wish you would have opened this on insights. Great vid.

  • @RishabhDeepSingh
    @RishabhDeepSingh Před 27 dny +1

    Damnnn this is high level c++

  • @cgazzz
    @cgazzz Před 27 dny +1

    It looks like only infinite generators can be done this way. Python uses GeneratorExit exception to dynamically end the generator, but exceptions shouldn't be used like that in C++

  • @cristian-si1gb
    @cristian-si1gb Před 11 dny

    Careful tho, this implementation technically returns you an input_range, not a forwarding one, since the sequence is technically single pass only. The resulting view doesn't reflect that tho, algorithms you pass this into might break

  • @Omnifarious0
    @Omnifarious0 Před 28 dny

    The modules in my StreamModule framework work in approximately this way. Each module can be thought of as a co-routine in which the state is in an object rather than on a stack.
    They aren't as convenient to use though. I should write a wrapper that allows you to use a single-input, single-output module as a generator in the manner you show here.

  • @BigPapaMitchell
    @BigPapaMitchell Před 28 dny

    This is very cool. Is there anything here using anything past C++20?

  • @konstantin2296
    @konstantin2296 Před 24 dny +2

    If there a way to reuse that lambda?

    • @cppweekly
      @cppweekly  Před 18 dny

      Certainly, just have a function that creates and returns the lambda, or copy the lambda (and its state) at any point along the way.

    • @konstantin2296
      @konstantin2296 Před 18 dny

      @@cppweekly yeah, exactly what I did. But hoped maybe there is a trick to default construct mutable lambda without factory function

  • @shawnzhong
    @shawnzhong Před 25 dny +1

    Can’t you use std::views::repeat instead of iota?

    • @cppweekly
      @cppweekly  Před 18 dny

      umm.... maybe? I didn't think about that.

  • @wojciechrazik
    @wojciechrazik Před 27 dny

    Nice exampleI think it would be worth to benchmark this with coroutine version!

    • @konstantin2296
      @konstantin2296 Před 24 dny

      You can do that but I can't even imagine a way how coroutines could be faster here. Clang literally optimizes it to print 10 fib numbers line by line.

    • @wojciechrazik
      @wojciechrazik Před 24 dny

      @@konstantin2296 Exactly! I would rather expect coroutine version to be slower. That's why it would great to measure it!

    • @cppweekly
      @cppweekly  Před 18 dny +1

      Yes, it would likely be orders of magnitude faster than the equivalent coroutine. you've probably already see the follow up episode now about that.

  • @nmmm2000
    @nmmm2000 Před 28 dny +1

    That's a hack I like

  • @thisisolorin394
    @thisisolorin394 Před 26 dny

    The typical use case would be to have a custom generator. Here that is essentially provided by iota

  • @FryGuy1013
    @FryGuy1013 Před 28 dny +2

    Overloading operator|() like this feels as wrong as overloading operator>> for streams.

    • @Nuclear868
      @Nuclear868 Před 27 dny +8

      It feels the same as the pipe in linux shells, which allows to pass the output of a command as input of the next command.

  • @davidhunter3605
    @davidhunter3605 Před 28 dny +1

    I think one huge difference to a std::generator function is that the std::generator is type erasing. This has huge consequences, your implementation has to live in a header file and users pull in all your implementation details. Basically this similar to the massive downside of header only libraries. A function that returns a std::generator can be declared in a header an implemented in a cpp file. Also the type of your return range is probably some hideously long thing which is fine until it appears in compiler errors

    • @syyyr
      @syyyr Před 28 dny

      you could use std::function as the parameter to do type erasing

  • @hasanrasulov3502
    @hasanrasulov3502 Před 23 dny +1

    Hello, Jason. I like your explanations. I’d be happy if you made a video about “how undefined behaviours is useful for optimizations”.

    • @cppweekly
      @cppweekly  Před 22 dny +2

      I have a place for viewers to request topics so I don't lose track of them: github.com/lefticus/cpp_weekly/issues/
      Feel free to add your idea and vote on the others on the list!

    • @hasanrasulov3502
      @hasanrasulov3502 Před 22 dny

      Cool

  • @broken_abi6973
    @broken_abi6973 Před 21 dnem +1

    It bothers me that coroutine-based generators couldn't be made malloc-free when this is possible...

  • @anon_y_mousse
    @anon_y_mousse Před 27 dny +2

    This is an interesting method, but requires too much boilerplate for my liking. I also don't think I'll ever like `views` or any of the functional programming constructs that they've added to C++.

    • @cppweekly
      @cppweekly  Před 18 dny

      I don't consider reusable functions to be "boilerplate" the generator function can be used with any callable you pass into it.

    • @anon_y_mousse
      @anon_y_mousse Před 18 dny +1

      @@cppweekly Just because something is reusable doesn't mean it's not still boilerplate. Consider how many other languages there are that make it easy to one-line such a construct.

  • @LogicEu
    @LogicEu Před 28 dny

    I think it would have made much more sense to have the lambda function actually use the parameter / argument, otherwise you're not really using the number generated by iota and all you're essentially doing is repeatedly calling your lambda from a for loop. As your lambda is actually holding state, it actually works as a generator.

    • @AlfredoCorrea
      @AlfredoCorrea Před 28 dny +1

      that is the point, general generator are not (easy) functions of the index they are called on. So all the state is in the lambda, there is no way the argument could have been used in this example, is there?
      (Fibonacci has a closed expression but that is not what is being illustrated here)

    • @LogicEu
      @LogicEu Před 28 dny +3

      @@AlfredoCorrea my big question is what is the point of the iota function in this example. It's supposed to pipe a number (increasing its value every time it's being called), but the fib lambda is not using this value at all. There's no point on using the iota function, or the transform function for that matter. Why not just call fib, being that it's already a generator?

    • @AlfredoCorrea
      @AlfredoCorrea Před 28 dny +2

      @@LogicEu I see, well yes, I think anything else could have been used, such as view::repeat with a dummy value. I guess iota is the simplest range and optionally you can use the index if you need it. Not sure if there can be a simpler range with less state. The compiler ideally will not even execute any code since the result is not used at all.

    • @fuxorfly
      @fuxorfly Před 28 dny +1

      @@LogicEu because you want a view. this is a cheap and easy way to create that view, and then replace the actual calculation guts with your own function.

  • @Dimkar3000
    @Dimkar3000 Před 25 dny +2

    Is it just me or this code is unreadable? A lambda wrapped in a generator function that uses 2 stl functions to pipe the values the result of the lambda around so we avoid compilation errors...

  • @Pelfik
    @Pelfik Před 27 dny +2

    This shit is much less readable than a normal c++ code. What is its purpose?

    • @fuxorfly
      @fuxorfly Před 27 dny +3

      They are reusable components that allow you to composably construct more complicated views of data. Say you wanted to take a vector, skip the first N number of elements in it, and then call a function on the next M number of elements on it. With a composable view, that becomes as simple as:
      for(auto i : v | std::views::drop(numSkipFirst) | std::views::take(numToUseAfter))
      {
      func(i);
      }
      If you wanted to do that without views, you would need an index variable, keep track of some math, and make sure you are indexing and iterating the iterators correctly. Its a lot more complicated:
      auto i = v.begin();
      std::advance(i, numSkipFirst);
      for(auto index = numSkipFirst; index < (numSkipFirst+numToUseAfter) and i != v.end(); ++i, ++index)
      {
      func(*i);
      }
      As soon as your needs get out of the realm of the toy slide-ware example, it becomes significantly more readable to use ranges and views.

    • @anon_y_mousse
      @anon_y_mousse Před 26 dny +1

      @@fuxorfly To be perfectly honest, both examples look like garbage and if the language treated arrays as first class objects it'd be a lot simpler without such disgusting code. Consider a language where they are first class and you could have: for i in a[N:M] { fun( i ); }. The meaning of which would be taking M elements from N forward, iterating that subarray and calling fun() on each element in turn.

    • @fuxorfly
      @fuxorfly Před 26 dny +2

      @@anon_y_mousse you don't need arrays to be first class objects to get syntax like that - views are the way to enable that. if you want to combine multiple view adapters into a single thing that is more concise you can. that's part of what makes view adaptors powerful. the other part is that you can use them with far more than just first class types; in this case, user defined types are enabled to be equally as powerful as language provided ones. example here is a vector, but it could be a map, a stream iterator, etc.

    • @anon_y_mousse
      @anon_y_mousse Před 26 dny +1

      @@fuxorfly It's added syntax that achieves a partial result of arrays being treated as first class objects. Keep in mind that you couldn't do range based for loops prior to C++11. Also keep in mind that a language treating something as a first class object just means that there's syntax added to enable easier usage of it as a concept. We still can't do [X:Y] syntax to acquire a range, nor can we do [X..Y] either, but if they add that syntax then the first class treatment will be complete.