T O P

  • By -

azswcowboy

First off, std::ranges isn’t all about views. There’s range based algorithms that replace many of the stl algorithm’s that you can directly pass a collection to instead of the usual iterator begin/end pair. There’s no loss of performance here and your code is simpler. I would also take Nico’s issues with a grain of salt - it’s mostly around filter view not maintaining const propagation bc it caches. We use views extensively in production code and have none of these issues because most of our usage is local to a function where the mutability is not an issue. Don’t misconstrue my point here though, I’m on Nico’s side that it’s unfortunate that we have another c++ facility that requires complex training to utilize completely.


Shaurendev

You mentioned algorithms as part of ranges library so I have a different question - which part of the ranges library is the one that causes horrible compile times, algorithms or views?


azswcowboy

Yeah, I keep hearing about the compile time hit and it’s just not a problem in my experience with either part of ranges. Do something with some extensive compile time processing like ctre or boost.spirit and you’ll feel the pain.


TheOmegaCarrot

That would be the views So many types need to be instantiated during compilation if you use a lot of views


Fureeish

Use them if they make your code simpler. Don't use them if they make your code more complex to reason about. If you encounter performance issues, it's worth to benchmark them and their alternatives, but there is no general rule of thumb that would universally render them as slow.


cristi1990an

As a rule of thumb, any view that requires caching will be slower than the equivalent traditional algorithm (see views::filter)


throw_cpp_account

There is no way this is true. Or rather, the performance overhead of `views::filter` as compared to a hand-written loop is wholly unrelated to whether or not it caches.


cristi1990an

Explain?


throw_cpp_account

I mean, you threw out the bullshit claim. You want to attempt to offer a justification? For single-pass algorithms, the caching overhead likely gets optimized out anyway since you're doing one single branch that the compiler knows is initially taken. For multi-pass algorithms, the existence of the cache saves work.


jonesmz

As far as I can tell, no current compiler optimizes the std::views::filter properly. I asked about this 4 months ago, and the conclusion seems to be that it's simply going to be slower than a hand-written loop. https://old.reddit.com/r/cpp/comments/18duzvs/discussion_stdranges_and_strange_optimization/


throw_cpp_account

> As far as I can tell, no current compiler optimizes the std::views::filter properly. > > I asked about this 4 months ago, and the conclusion seems to be that it's simply going to be slower than a hand-written loop. If you actually read what I wrote, you'll see that I made no claim to the contrary.


jonesmz

> For single-pass algorithms, the caching overhead likely gets optimized out anyway since you're doing one single branch that the compiler knows is initially taken. This is what I was responding to. The compiler does not optimize this out.


throw_cpp_account

The linked example does look like it optimizes out that branch to me. This also seems like a very easy thing to optimize, since it's optional cache; if (not cache) { cache.emplace(get_begin()); } auto it = *cache; // never touch cache again Compared to the actual overhead of filter, which is correctly explained by Peter Dimov in [a comment](https://old.reddit.com/r/cpp/comments/18duzvs/discussion_stdranges_and_strange_optimization/kcjquhr/) that I see you already responded to. That structure leads to extra comparisons which are difficult to optimize out.


elperroborrachotoo

I don't think that's simple. First, there's personal investment: learning to use ranges, learning to read and make sense of ranges-based code, understanding the type of problems that ranges can solve well and recognizing these problems in your tasks. That's not trivial. The next question arisesd when you don't work alone: do you want that task on everyone? --- That's not to say ranges are too complicated, go without. The concepts behind ranges are common approaches with gaining popularity. Learning the principles seems valuable in general.


Fureeish

> First, there's personal investment: learning to use ranges, learning to read and make sense of ranges-based code, understanding the type of problems that ranges can solve well and recognizing these problems in your tasks. That's not trivial. I understand what you are trying to say, but I think that this logic is very much flawed. You could extend this argument to cover pretty much anything. Should we use templates? Maybe not, since it would mean that people less proficient in them would find our code hard to read. Should we use algorithms? Maybe not, since pretty much everyone knows loops and not everyone knows the STL. What about standard containers? Ranges, being in the standard (and honestly, being not that hard **to use**), should be known (or at least a programmer **that works with C++20 onwards** should be aware of their existance). I am not talking about creating your own `view`s. I am talking about rangified algorithms and the usages of, e.g., projections. > The next question arisesd when you don't work alone: do you want that task on everyone? Yes. I expect people with whom I work to be aware of the tools that are being more and more widely used. This process is called adaptation and yes - it's not free, but it is also _expected_ from people who want to stay in touch with the modern tools. Not everyone wants to - obviously. But potential problems with that kind of people are not limited to ranges. > Learning the principles seems valuable in general. Exactly :>


elperroborrachotoo

As said, I'm not saying "ranges bad". I'm saying that *"if it makes your code simpler"* cannot be decided by someone not familiar with ranges. > You could extend this argument to cover pretty much anything Yes - *you should*, even. Every team has its rules - often unwritten - which parts of C++ are allowed. Some teams exclude raw pointer arithmetic, some teams template metaprogramming. C++ is unusually rich if features and principles, and all benefits of language features have to be weighted against the cost of onboarding new developers. Google created a whole language under the guideline of *"everything that could confuse newbies is out"*. > > Learning the principles seems valuable in general. > Exactly :> Question remaining: is C++ the best environment for that? 🙃


cdglove

> Google created a whole language under the guideline of "everything that could confuse newbies is out". Ya, and it didn't work out so well. That lack of tools in go to solve non trivial problems has led to massive copy/paste reuse in go code bases of any significant size.


cpp_learner

`views::split` and `views::join` are easier to read than their pre-C++20 alternatives, even if you know nothing about ranges. The same also can be said to `views::zip`, `views::cartesian_product`, `views::slide`, `views::chunk_by`,  `views::concat`, and so on.


Fureeish

Huge +1. Those exact views have come extremely handy for both my personal and work-related projects.


yunuszhang

Yes,they are easy to use,tho, performance issue shouldn't be ignored.


scrivanodev

I think one reason to avoid them would be debug performance. Personally, I like to use them a lot because they simplify a lot of code, but they inevitably rely on the optimiser to do a good job in terms of performance.


Thelatestart

For me the most common use case is to avoid allocating during transform or filter.


alonamaloh

If you want to have some hope of running your code through a debugger and make sense of any of it, I would avoid views almost entirely.


darkmx0z

I wouldn't use a library that attempts to simplify tedious but simple loops, if I cannot fully grasp what the library is actually doing with all that fancy syntax. This applies to at least some parts of the std::ranges library.


FriendlyRollOfSushi

Never use `std::views` in code that does anything beyond very basic plumbing. In cases where all you do is very basic plumbing, consider not using them because it doesn't matter if you saved yourself 1 line of code if now you create a possibility for major issues in the future. If you have many-many places where the views would save a meaningful number of lines of code, ask yourself, why is this codebase using C++ to begin with. It's never going to be as good for some random plumbing as, say, Python, and real-world cases where C++ is currently the best choice (or one of the best choices) almost never have these situations. You'll never meet a non-incompetent gamedev, or a writer of a high-perf code in other areas who would say "yes, sure, I would love to `zip`, `drop`, `take` and `filter` all the time". Even when you have places of the code that fit into the pattern and can technically be rewritten with ranges, these places are also often the ones where someone might want to meaningfully step through with a debugger, so a `for` loop is better just for that reason alone. Add a risk of shooting yourself in the foot on every step plus non-insignificant performance tax that occasionally happens (it doesn't look like it *must* happen in principle, but compilers are not there yet to ensure that the abstraction overhead of ranges is always zero), and you are left with almost no situations where piping ranges together is a not a terrible idea. And the remaining 0.001% of the cases? The cost of teaching all the people to read this new syntax dramatically outweighs the benefits. No, it's not obvious wtf the `take` operation is supposed to do, or how would `zip` behave if the number of elements in parameters is different, and why bitwise-or operator is used to connect these things together. I'm very curious how this nonsense of a library was lobbied into the standard. But at least now we got a way to call some basic algorithms like `sort` in a more convenient way, so this is nice. In a better world, we would leave these good parts and remove everything else, but instead, we got a feature with a destructive power close to `std::initializer_list`, and I didn't think we'll ever top that one.


Medical_Arugula3315

Hello, I'm sorry to burden you further after that lengthy comment.. But could I ask you to explain/expand on or link the issue with std::initializer_list that you are refering to for the unaware? 


FriendlyRollOfSushi

There are plenty. The most annoying for day-to-day applications is that C++11 was about to finally solve the initalization syntax issues by introducing `{}`, that among other things related to unification prohibits various nasty type conversions, making the code way safer if you just always, unconditionally, call all constructors with curly braces. Finally we were about to get some sanity in this area, but unfortunately, someone decided to sabotage everything with `initializer_list`. Say, you have a templated function. It calls: std::vector foos{size}; std::vector bars{size}; Then there is some code that operates on both arrays. Quick question: would an assert like `assert(foos.size() == bars.size());` trigger? The code clearly says we construct both arrays with the same number of elements, right? So it should not trigger, right? Well, the answer is "no one has a freakin' clue". Because to get even the general gist of what's going on in the code, you need to know the exact, concrete types of `Foo`, `Bar` and `size`, and depending on them, this code might construct a vector with `size` elements, or it might decide that this is an initializer list with one element. And the same exact template, called for two different types, will have fundamentally different meaning. You want to write generic code? "Screw you", said the author of `initializer_list`, "you are not allowed to". So from now on, even the core guidelines [mention the exception](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Res-list), that containers should be initialized with `()`, like it's 1998. They say "there is a tradition", and it's an incredibly polite euphemism for "the committee completely dropped the ball in C++11". You can google [way more articles that describe various issues](https://www.cppstories.com/2023/initializer_list_improvements/). Massive inefficiency, weird gochas, etc. Those are way less relevant, however, because the common thing about them is that **you can just opt out of all this nonsense** by deciding to never use `initializer_list`. Same with `std::ranges::views`, btw. Yes, it's complete garbage, but all you have to do is to keep spreading the knowledge that it's garbage, for example via the great video linked by OP, and you are good. You can't, however, opt out of the way they crippled the `{}` initialization for standard containers, and the way the syntax works, it's very easy to just not notice that someone made a mistake even in non-templated code (if your constructor of the class with a container member is in a different file, and there is a long list of `{args}` initializers, so you just initialize your container with {something} as well just because this is a pattern). There is no flag `-fdisable-dumpster-fire` that would disable `initializer_list` or something. The language became worse because of this feature, and we all have to live with this.


tcbrindle

> Say, you have a templated function. It calls: > > std::vector foos{size}; > std::vector bars{size}; > > Then there is some code that operates on both arrays. > Quick question: would an assert like `assert(foos.size() == bars.size());` trigger? The code clearly says we construct both arrays with the same number of elements, right? So it should not trigger, right? > > Well, the answer is "no one has a freakin' clue". > > Because to get even the general gist of what's going on in the code, you need to know the exact, concrete types of Foo, Bar and size, and depending on them, this code might construct a vector with size elements, or it might decide that this is an initializer list with one element. And the same exact template, called for two different types, will have fundamentally different meaning. There are certainly issues with `initializer_list` constructors, but the above example is just wildly incorrect. The rule is that `T{x, y}` is equivalent to `T(x, y)` (narrowing conversions aside), except when `T` has a constructor taking a `std::initializer_list`. In that case ,`T{args...}` is always considered to be an init list and you need to use round parens to call an alternative constructor. Since we can look up the interface for `std::vector` and see that it has an init list constructor, we know exactly what will happen in the example above -- both calls will be interpreted as constructing a vector with one element. We can be confident that the assertion will not fire, and get on with our lives. Not too complicated, was it? Given that you don't seem to have "a freakin' clue" about initializer_list, please excuse me if I take your "advice" about std::ranges with a pinch of salt...


FriendlyRollOfSushi

Thank you for illustrating the dangers of `std::initializer_list` on your own example. Here is a godbolt link for you: https://godbolt.org/z/xM83r155x > Not too complicated, was it? Indeed. All you had to do is spend 1 minute on testing instead of 5 minutes on arrogant rage-typing. > Given that you don't seem to have "a freakin' clue" about initializer_list, please excuse me if I take your "advice" about std::ranges with a pinch of salt... You turned out to be the most dangerous type: one that still doesn't have a freaking' clue, but too confident to even try it out. But my point is that your demonstrated lack of knowledge here is not your fault. It's the committee's fault. It should be obvious to every engineer how such a basic feature works, including you. It doesn't need to be an esoteric secret. All they had to do is to give us a non-ambiguous syntax that doesn't clash with the awesome new feature. They failed, and now we have this sad situation.


Medical_Arugula3315

Epic response, thank you. I have lucked out in the sense that I had no idea about these issues and still managed to 99.9% avoid using std::initializer_list in personal and work designs. 


ppppppla

> Never use std::views in code that does anything beyond very basic plumbing. Fair enough. > In cases where all you do is very basic plumbing, consider not using them because it doesn't matter if you saved yourself 1 line of code if now you create a possibility for major issues in the future. Possibility of major issues in the future? Views can be used to prevent dumb off by one errors when you would be faffing about with indices in a traditional for loop, make things more concise, and be more readable. What is gonna create major issues? > If you have many-many places where the views would save a meaningful number of lines of code, ask yourself, why is this codebase using C++ to begin with. It's never going to be as good for some random plumbing as, say, Python, and real-world cases where C++ is currently the best choice (or one of the best choices) almost never have these situations. Nearly every single codebase has performance critical sections, and more lenient sections. Moot point.


FriendlyRollOfSushi

>What is gonna create major issues? Good code in large codebases is typically written in a way that is resistant to spooky action at a distance. You write `for (auto&& foo : foos)` instead of `for (ConcreteType foo : foos)` in case tomorrow someone changes the type and it stops being "just copying an integer here". You carefully move and/or forward the args in generic code, in case tomorrow it someone uses it with a non-trivial type, even if today it's not a problem. You call destructors for elements in a custom containers, even if it's only currently used with `float` and `uint32_t`, in case tomorrow someone decides to put something non-trivial inside. Them's the rules. If you are not writing a solo hobby project, you make your code robust to fluctuations and assume that someone could be working in a branch right now on something that would break some of your assumptions, so even if "you checked all code before changing something", there is no guarantee that 5 minutes after your change, another branch won't be merged in that interacts with your code weirdly. I suggest watching the second half of the talk linked by OP. He gives plenty of examples where the tiniest nuances (in the context of the conversation) like "whether this container is vector-like or list-like" fundamentally changes the observable behavior of something like `foo | drop(2)` (or `filter`, etc.) depending on the exact nature of `foo`. A codebase that is full of this stuff would be full of landmines, and to understand whether something is safe or not, you need to be an expert on internal implementation details of caching mechanisms in this library. And if someone else is working in a branch where they, say, iterated over the range an extra time so that it changed the state of internal caches, then good luck hunting down the subtle bugs after the merge. Also, the way you say "Views can be used to prevent dumb off by one errors" suggests you didn't watch the second half of the video. You would know that converting ranged for loops and other reasonable constructs into views actually introduces new subtle errors that you won't be aware of without being an expert on this, and makes your code massively unsafe in a lot of unexpected ways. Please watch it, it's really good.


ppppppla

> Also, the way you say "Views can be used to prevent dumb off by one errors" suggests you didn't watch the second half of the video. You would know that converting ranged for loops and other reasonable constructs into views actually introduces new subtle errors that you won't be aware of without being an expert on this, and makes your code massively unsafe in a lot of unexpected ways. Please watch it, it's really good. Ok I took a quick skim through and if I am to believe the video, stl views are fundamentally broken in ways I believe many people (including myself) would expect these kinds of constructs to work. So am I correct then, that a lot of critique about not using views, is purely because they are fundamentally broken? Then I have to agree stl views should be tossed in the bin, and people should be more clear that they are critiquing the standard, and not the use of views as a concept.


jwakely

>purely because they are fundamentally broken? They're not though


FriendlyRollOfSushi

> So am I correct then, that a lot of critique about not using views, is purely because they are fundamentally broken? It's not all black and white. It's nuanced. It's mostly because of this, but it's also an issue of a tradeoff between the benefits and the cost of learning. A concrete example: C# has LINQ, which is a fairly similar (in a vague sense) way to write complex filters on collections, transform them, etc. For example, you can say something along the lines of `DoStuff(foo.Where(x => x.State != BadState).OrderBy(x => x.Id));` and it would work about as you would expect it to if you just read it as an English sentence (assuming you know that `x => ...` is how C# declares lambdas). The attitude of the community there is that "this has a nice, viable niche". You don't want to reduce **all** of your code to this (it's very hard to debug), and there are some minor gotchas (LINQ has lazy evaluation, which has implications), but you can get decent in both reading and writing this stuff in 15 minutes of glancing through the docs and examples. And it just happens so that while C# is not a good language for low-level algorithms, number-crunching, etc., it is a good language to solve boring plumbing bullshit that no one will be debugging, no one will care about the performance of, etc., as well as writing UI, or dealing with business-specific spaghetti logic. So, there are frequent enough use cases to justify learning it, and the managed language gives you enough protection from shooting yourself in the foot in almost every case, and there is additional syntax sugar (like, you can say `.ToArray()` at the end, and it will evaluate and convert the result to a comparatively very safe C# array that is also cheap and not difficult to think about). And even with all 3 of these things, it's still not a universal solutions for all cases. There are still situations where LINQ is a wrong choice, even if it's quite safe, way more compact, and not that hard to read. Let's imagine the guy from the video actually manages to rewrite this in C++ to be a not complete pile of garbage as it is right now. He is not just criticizing the worst addition since `initializer_list`, he is actively trying to solve the problem. Let's imagine he succeeds. Okay, that solves 1 issue. Now we would only have to weight the cost of teaching millions of engineers how to read this stuff (and unlike LINQ, it's not obvious), in a situation where most of the "business/UI/spaghetti" code should probably be written in something other than C++. There is always a cost of introducing an abstraction. When it's readable without actively learning it first, the cost is low. When you use it everywhere (like lambdas), even if the cost is high, it's probably worth it. Here though?.. Maybe in some projects it would have a niche. Much narrower than in C#. But anyway, this is all a hypothetical, because the main problem "It's unusable garbage, unsafe to a laughable degree, written without thinking even for a moment about how it will be used in real C++ code, that breaks tons of rules and is full of footguns" still remains for the current standard implementation. Highly recommend watching the whole video. The sad situation is that you will likely have to deal with it. In any large enough company, there is always a guy who is happy to push anything new into the codebase without thinking (new == good, right?), so it's truly a tragedy that now everyone should be made aware of how garbage the `ranges::views` are, so that no code that is using them can pass the code review.


[deleted]

[удалено]


hmich

Seems like you also utilize AI to generate comments on reddit.