>It makes lots of important things unnecessarily hard and often leads to unnecessary copying [1]
If you read the article referenced by that thread you'll see that a major issue with string copying is due to having a lot of bad interfaces taking raw C pointers so code at both side of the ibterface need to make copies exactly because the raw C pointer doesn't guarantee exclusive ownership. The remaining issues are due to a badly optimized string buider in Chrome and failure of pre-reserving vector memory which lead to many copies on resize. This last issue is fixed with move semantics in c++11.
>It makes it hard to even do something like OCaml's List.filter and Array.filter
Std::remove_if works genrically on any range. Boost (and the range TR) provides iterator views when you need lazy evaluation.
> You can build an entire transactional model on top of higher order functions.
I'm sure you can, in the end you can implement anything manually. With RAII propagation of lifetimes is done automatically by the compiler.
>Using heap storage leads to the aforementioned problems, where object lifetime can become unpredictable.
Only if you use shared ownership. Otherwise is completely predictable.
> Take a Map or Dictionary for example. C doesn't technically speaking provide one, so you learn to implement them pretty quickly and often.
I take issue with the "quickly and often" part, but you do learn to implement them! And that provides valuable insight when it comes to other languages.
I've had senior devs who, like past me, didn't understand why removing an element from a vector or map invalidates iterators or indices on it. I learned precisely why when implementing vectors in C: the usual implementation of those collections shifts everything around to maintain element contiguity in memory, so now your iterator/index is pointing to a different element (which incrementation will skip) or possibly past all the elements!
And when you're presented with these fancy data structures in Java or Python, that feels unintuitive and wrong. "Come on; I should be able to just iterate through this and remove certain elements as I go," like you would remove objects from a shelf. The shelf doesn't rearrange the objects after you take one out! But the reason it feels like an affront to common sense is that you've been trained to take the fancy data collections for granted and not forced to understand the complexity behind them that makes them efficient.
Of course, the real answer is to do that with some functional algorithms, like `std::remove_if()`, instead of via iteration. That way, you'll really be in the cloud-scraping ivory tower and never have to think about what's going on below. https://youtu.be/ggijVDI6h_0 :p
The problem is that you need a vastly more complex machinery to cover all the various ways to avoid copying/reference counting and then still don't have a general solution when you can't avoid multiple ownership (unless you count std::shared_ptr with its very high overhead).
As I said before, it's not that you can't do it, it's that there are costs associated with it.
> If you need both the original list and the the filtered list, in a functional language you need to allocate new cons cells anyway.
You can filter arrays also and allocating cons cells for a list is pretty cheap with a modern GC, as discussed before.
"A key innovation of C++ was to introduce RAII, which essentially ‘piggybacks’ on the value of the stack and enriches it with a lot more power."
I haven't written C++ in two decades, but it was my impression that heap allocated resources are not on the stack.
The major problem with C++ isn't resources can be used used after freed, it's that they are never freed if you don't clean up when your RAII is popped off the stack via destructor or other "event handler methods".
Isn't RAII more of a pattern implemented atop the core constructor/destructor? It's kind of a stretch to call it a fundamental feature of C++ like GC is to java/jvm.
Has valgrind "solved" memory leaks as a primary concern/danger in C++?
I'm responding to a comment that claims all lifetime issues are solved by RAII.
My argument was that for efficient code, you need to pass references or pointers, which means you do need to care about lifetimes.
And your argument is that's not true because we now have std::string_view? You do realize that it's just a pointer and a length, right? And that this means you need to consider how long the string_view is valid etc., just as carefully as you would for any other pointer?
That's a fair criticism, I think C++ blurs a lot of lines that make these things difficult to talk about in isolation, and I'm still working on being more rigorous about picking them apart correctly. The very specific complexity at the core of it all is the fact that you need something like placement new to begin a lifetime in memory that is already allocated. Copying the object representation of an initialization template is insufficient in general, you have to use a specific tag to say "the life of a new object starts here", which has no direct analogy to anything the hardware does. It's not necessarily important that the programmer can hook into this like a C++ constructor, but the tag needs to be there. This is something that can be very difficult for programmers to get right in languages that aren't Rust, because the compiler does no verification of it. If you do it wrong, everything works in debug, but then things may break in release because the optimizer performed incorrect alias analysis or detected unconditional UB. But they might work correctly, you may not notice for years until a more powerful optimizer comes along. Since we don't plan to validate lifetimes (Rust is a great language that already does that if it's important to your goals), we would like to avoid this sort of strict object model as much as possible.
When you add RAII on top of lifetimes as a language feature, it creates the need for language support for moves and specialized copies. Or a need to say that types cannot be copied. But you need some sort of tag that says "memcpy doesn't cut it anymore", which is what I mean by overridable copy behavior.
> C++'s philosophy is zero-cost, opt-in abstractions.
It's a nice philosophy, but unfortunately C++ itself often fails to deliver unless the programmer is an absolute expert on the underlying semantics. E.g. forget to pass a vector by reference or as a pointer, and you get a complete O(n) copy underneath. With other data structures, one can implement efficient copy constructors to make this pretty cheap. When an abstraction leaks details of the underlying implementation that then lead to huge non-obvious costs, that is not an abstraction.
Another example is that C++ heap allocation relies on an underlying allocator, which has overhead in managed chunks of memory, size classes, etc. The underlying allocator's alloc() and free() operations are not constant time. In fact, they are almost always outperformed by the allocation performance of GC'd languages, where bump-pointer allocation and generational collection make these overheads very, very cheap.
1) It's useless to reason about the performance of generated assembly unless you use -O{1,2,3}. Another commenter says that they're surprised std::move isn't inlined; that's because -O{1,2,3} isn't passed.
2) If you care that much about performance, you can avoid all copies by returning by reference in this case.
3) The author seems to misunderstand std::move — in this case, std::move can only pessimize performance and never optimize because the move constructor of std::array is the exact same as the copy constructor.
For example, move constructors help performance for std::vector because the move constructor will copy the backing buffer's pointer instead of copying the entire backing buffer itself.
Since std::array<T> is a thin wrapper around T[], the entire array is stored on the stack, and there is no possible optimization that a move could make — stack values must be copied (or otherwise initialized) in move constructors. In fact, since std::array is an aggregate type, there isn't even a user-defined move constructor (or copy constructor)! https://github.com/gcc-mirror/gcc/blob/master/libstdc%2B%2B-...
My criticism stems more from C++'s steadfast refusal to drop backwards compatibility, in any way, for anyone, ever -- while also adding new features. What this means is that new features can't provide the guarantees they can in other environments leading to "ruined fresco" [1] syndrome.
Concrete example: std::move. Move constructors can copy, and `std::move` doesn't move. Naturally, it just casts your T onto `std::remove_reference_t<T>&&`. Because why not. It also leaves your source object in an undefined but totally accessible state -- whose validity is up to the implementor's convention! I think std:: collections are totally useable after they've been moved (correct me if I'm wrong) but your own types may just explode or fail silently. Talk about a giant footgun.
This approach leads to poor imitations of features from other languages getting stacked on top of the rickety footbridge that is K&R C.
It's specifically the evolutionary design philosophy that I take issue with.
The language has become borderline impossible to reason about. Quickly, what's the difference between a glvalue, prvalue, xvalue, lvalue, and an rvalue?
And the compiler, in the name of backwards compatibility, sets out not to help you because adding a new warning might be a breaking change. I've got easily 15 years of experience with C++ - granted, not daily or anything. To figure out what's actually happening, you need to understand 30 years of caveats and edge cases.
This leaks argument is really getting old. I have no leaks in my code. C++11's addition of move semantics and using references everywhere makes pointers unnecessary for the most part. You can use STL containers for putting your items into so shouldn't see raw "new" or "delete" operations in your own code very much; this is particularly true where you define your own move operators and move constructors.
Even "old" C++ should have no leaks if you use RAII properly, have clearly defined container classes, use references everywhere or const pointers if you must. If you write sloppy code, you get sloppy output.
See Stroustrup's "The C++ Programming Language Fourth Edition" (the blue book) section 3.3.3 Resource Management and 3.2.1.2 "A Container", where in this early part of the book Stroustrup explicitly directs to 'avoid "naked" new and delete operations" and to "use resource handles and RAII to manage resources".
EDIT: Wahay getting downvoted - thanks!
In any other language (eg. PHP) if there was a vocal crowd complaining about how their scripts are slow when they do something stupid like fetching an entire database table and then doing filtering within the PHP script itself, everyone would say "But you're doing something stupid - it is going to be slow" and nobody would argue with it.
With C++, when you point out that someone is doing something foolish, you get downvoted and people start making arguments about features of the language instead to detract from the truth that you've highlighted, ie "but a good language wouldn't let you do dangerous things", which is the same as saying "knives can cut you - ban all knives!!". It's really wearisome, and always rears its head here on HN where C++ is NOT the language of the day.
I'm not saying the interface is externally bad. The danger and reasoning here is that the implementation could be made bad. STL functions can tolerate that risk, and they don't have the luxury of knowing the object's a std::string on an uncritical path, not some graph node with zillions of back-pointers. (Edit: I think their interface also avoids copying when dealing with non-movable types.)
> don't use move on an object that's doing RIAA to take and release a lock.
Absolutely. If the question is "what would I do in C++ today" then my answer is that I would use RAII subject to common-sense rules like this.
> Explicitly disable those actions (private copy constructor and operator, private move)
That's a decent enough solution within the context of C++ as an unchangeable thing, but I want people to look beyond that context. C++ has never been an unchangeable thing. Programmers shouldn't have to take such extra steps, so closely tied to how the language works that year, to get a conceptually simple result. That's how bugs creep in, and how even correct code becomes drudgery. We have no choice but to accept it now, but we shouldn't accept it permanently.
> In order to have a reasonably complete language that has RAII and value types, you must also have: - constructors - destructors - overloadable copy assignment operators - placement new - move semantics and rvalue references
C++ didn't have move semantics at all until several decades after it had RAII.
>The problem is that they introduce irregular memory layout.
Good point. I thought that the original thread was about pointers eating up space.
My understanding is that C++ class definitions, because they themselves may contain function references...even just constructors/destructors and access methods, would represent the object with a pointer on the call stack to the location of the object (and all its data + method pointers) on the heap (which like you said could end up anywhere resulting in an irregular memory layout). Otherwise, suppose a class is defined as a collection of objects (which are defined as collections of objects on down etc.) some of which may of arbitrary length...ergo you'd never know how much memory to allocate a priori to hold these irregular data structured. Far easier to just allocate a bunch of word length pointers pointing to whatever random blobs of address space the OS gives back on malloc requests.
But yeah, if it's just POD then one would assume an easy optimization the compiler could make would be to just create the objects as contiguous blocks of object-sized memory. I'm all rusty on some of this stuff (last time I seriously used C++ Borland was still a major player in the compiler business and templates were highly experimental) so I'm sure I'm quite out of date these days.
RAII sucks the way C++ does it. Magical background BS with massive unintended complexity consequences requiring obtuse intricate crap like the copy-and-swap idiom, a mudball of pointer and reference types, etc. They should have added a defer/scope-exit statement and been done with it.
> When you move an existing object (by specifically saying std::move), what should happen with it?
You don't destroy it, you treat it as an actual "move", where the compiler will not consider the original variable as accessible after this point, and not allow accesses after the move. The memory it took up is free for reuse, and no destruction code is run on the side of the code doing the moving (in the case of a conditional move this gets a tiny bit more complex, but not too much). "It's variable will be still accessible in local scope" is exactly what I'm getting at; you can enforce at compile time that this isn't the case by simply disallowing access.
You shouldn't have local references active, just like how you shouldn't have local references to the contents of a vector when you push to it. You can already invalidate local references to a part of a struct when something gets moved in modern C++. Being wary of invalidating local references is an established concept in C++; this doesn't exacerbate that problem.
It's even better if you track scopes in references which gets rid of the dangling reference problem entirely but at this point you've reinvented Rust :p
To be clear, I'm talking of a completely different model that could have been used in place of the rvalue reference and move model. Making C++ have linear types now would be tough, but it could have been done before. There are different tradeoffs there, but I suspect it would have been safer.
> all variables should have a "not-a-value" state and throw exceptions on use.
You can make this a compile time error. Like I said, linear typing is a pretty well established pattern.
I'm not sure what your point is. What part of rwj's argument don't you like? Do you dislike RAII? Personally, it's one of the few things I think I would miss from C++ if I went back to low-level programming. Even if file handles, etc are technically easier to handle, do we really lose anything by using a general tool to manage them, at least sometimes?
I feel like I'm missing something. Please explain.
If you read the article referenced by that thread you'll see that a major issue with string copying is due to having a lot of bad interfaces taking raw C pointers so code at both side of the ibterface need to make copies exactly because the raw C pointer doesn't guarantee exclusive ownership. The remaining issues are due to a badly optimized string buider in Chrome and failure of pre-reserving vector memory which lead to many copies on resize. This last issue is fixed with move semantics in c++11.
>It makes it hard to even do something like OCaml's List.filter and Array.filter
Std::remove_if works genrically on any range. Boost (and the range TR) provides iterator views when you need lazy evaluation.
> You can build an entire transactional model on top of higher order functions.
I'm sure you can, in the end you can implement anything manually. With RAII propagation of lifetimes is done automatically by the compiler.
>Using heap storage leads to the aforementioned problems, where object lifetime can become unpredictable.
Only if you use shared ownership. Otherwise is completely predictable.
reply