> As early as 1981, I pointed out that by reducing the number of objects that I had to keep track of explicitly from many tens of thousands to a few dozens, I had reduced the intellectual effort needed to get the program right from a Herculean task to something manageable, or even easy.
Fast forward a few decades, and we're still very much on this journey of finding the right abstractions/interfaces/libraries/languages. I feel like there must be a complexity equivalent to Parkinson's law: complexity expands to fill the space left in between abstractions.
I think more people need to see this. This is how the creator of C++ thinks we should be writing code. This is what he thinks code should look like. To split a string by whitespace we should use `while (cin >> s)`. We should have a `typedef` in the middle of functions. Iterations should use `.begin()` and `.end()` everywhere. There might even be a bug with a trailing "+" appearing in the output?
Imagine if this was a new language that the dev community was seeing for the first time. It's hard to imagine it gaining much traction.
If you want a laugh, Google a tutorial for how to read a file. You should also know that all the tutorials are wrong, because they fail to handle at least one footgun or another.
There is no “modern” alternative. If you read Reddit threads, C++ programmers actually believe that it’s a reasonable file reading API.
Most companies that I’ve worked at have just implemented our own on top of the OS syscalls. Which is annoying because it requires at least a Windows and UNIX variant.
Look, I like C++. I’ve been programming in it for years. But some of the stereotypes around C++ programmers are true. I still occasionally run into design decisions so untethered from reality that it still shocks me after all these years.
The FAQ was mostly written before C++11 with some updates since then. I don't think he is rewriting every code snippet to match modern styles. It is an enormous FAQ and not meant as an introduction to the language.
> Imagine if this was a new language that the dev community was seeing for the first time. It's hard to imagine it gaining much traction.
But it's not a new language. It's backwards compatible with C.
So "iterators" behave the same as pointers, since that's how you'd iterate through an array. You can add and subtract, then pass them to other functions.
You can't just have a function that returns a vector of strings, because that function would do an allocation. When is it deallocated? Before unique_ptr (the guide was written before), it'd be the caller's responsibility to manually do so.
Meaning you have to assign the output of that function to a variable every single time and manually remember to deallocated it or you get a memory leak.
C avoids this with `strtok` by destructively modifying the string in place. This is arguably worse.
If you were designing a new, non-GC, language, you'd have good ownership semantics and not allow pointer arithmetic. That'd be Rust.
auto_ptr was so misdesigned, they erased it from the language in a later standard.
The problem was that the assignment operator “T x = y;” is supposed to have copy semantics, but auto_ptr gave it move semantics by overwriting the source to destroy that reference. This broke all sorts of code that rightfully assumed assignment is done by copying. Therefore all kinda of algorithms like sort broke, as well as resource management. Suffice to say I am personally not a fan of C++ language design or Stroustrup’s opinions on programming.
I have some very smart friends who think it's the perfect language, but I kind of prefer almost every language that has come out after C++. I feel like the language adds some very strange semantics in some very strange places that can be hard to reason about until you've spent a lot of time with the language. This wouldn't necessarily be so bad if not for the fact that most people who write C++ have not spent sufficient time to understand it (and I consider myself in that group, though I don't write C++).
I have mixed feelings on D, but I'm very grateful that Rust came along. Rust is arguably even more complicated than C++, but the good thing is that getting a lot of these complications wrong will simply not allow your code to compile. It's a huge pain in the ass at first but I've become grateful for it.
I still write C very occasionally, but Rust has supplanted like 95% of jobs that were formerly C. I still really need to play with Zig.
Personally I use C or something, anything other than C++ really, if I need something more ergonomic or provably correct. Many excuse C++’s design history with “they didn’t know better”, but the oft forgotten history explained in that video shows otherwise.
I haven't played with Zig yet, but it does look like what I want C++ to be; something unapologetically low level but given some high-level constructs for safety and ergonomics. I haven't had a huge impetus to learn it because Rust has been happily occupying that space for me but it certainly can't hurt to play with Zig.
I should write an Ffmpeg codec with it or something this weekend to try it out.
I've been having fun trying out Zig recently; would recommend. FYI that they released a big batch of API changes in April (version 0.16.0) so be mindful of learning from old resources.
Would you say Rust is more complicated than C++, or just harder to write? I think that complication a la C++ where there are too many features for the language's own good is a different problem than being a clean language but with higher order features that require a bit more, I don't know, focus?
I make a strong distinction between complexity, the inherent amount of moving parts in a thing, versus complication, the amount of frippery involved in dealing with the complexity.
Rust is complex. It's solving complex problems. It's not complicated, though. There's not much you could remove without creating leaky abstractions.
In my opinion, C++ is equally complex. It's solving the same kinds of problems as Rust (although it'd be fairer to say "Rust is solving the same problems as C++"). However, it's hella complicated. There's a vast number of twists and turns to keep in mind if you want to use it, and most of them are things you could not have anticipated by reasoning about it from first principles.
If you took the design goals of Rust, and reinvented it from scratch, it'd probably end up looking a lot like Rust. If you were to reinvent C++ from scratch, I bet it wouldn't remotely resemble modern C++. If anything, I bet it would also end up looking like Rust, and the fractal of powerful footguns would be left on the cutting room floor because "that's insane, there's no way anyone would want that".
I think there are certainly different complications in Rust than C++. The RAII can be roughly the same in the naive sense, but there's having to worry about Send and Sync and Pin and fighting with the borrow checker and all that fun stuff.
Gauging how "complicated" a language is somewhat subjective, so it's kind of hard for me to give a straightforward answer. I think it's certainly easier to be (some definition of) productive with C++ than with Rust. I feel like to do anything even remotely non-trivial with Rust, you kind of have to understand everything, because if you don't do it in the "Rust way", it often won't compile. I think this is a good thing, but it does make it harder to get started.
C++ has a lot less consistency and (kind of) more features, and lots of strange semantics to go with those features, and so if people actually use them it can get confusing and hard to read pretty quickly.
My knowledge is a bit out of date, to be clear; previously whenever I need something in the C++ domain, I could fairly easily just reach for C and use that instead. Now Rust is available and I think overall better (though I do sometimes miss how utterly simple and dumb C is).
> having to worry about Send and Sync and Pin and fighting with the borrow checker and all that fun stuff.
To be fair, the alternative to having to worry about Send/Sync/Pin is not "not worrying about Send/Sync/Pin". It's having to worry about correctly enforcing the constraints they describe on your own, without any kind of mechanical help. E.g., not moving data to another thread that shouldn't be and not accessing data from multiple threads that shouldn't be. This stuff is intrinsic.
In this sense the Rust mental model is simpler, because failing to uphold these constraints is no longer "your fault", it's Rust's fault.
I don't disagree; I was coming from Java most recently, and a lot of the equivalents of "Sync" and "Send" were just mental notes I was making myself, and I wasn't really used to it being encoded into the type system.
> have to understand everything, because if you don't do it in the "Rust way", it often won't compile
I confess I haven't dug into it much yet, but this reminds me of how Haskell was. By the time you got a program to compile your project was more or less done.
In some senses I actually find Rust a bit harder than Haskell. With Haskell the types are immutable and as such they can be happily shared everywhere. That required basically no rewiring in my brain.
With Rust, I had to get used to single ownership or explicit cloning. There's an argument that this is "better", but I found it a bit harder to learn.
Rust is similar. I don't have a link handy, but I've read a few articles noting experience with programming in rust where a program that compiles works. Provided the logic is correct, of course.
> I do sometimes miss how utterly simple and dumb C is
You can make an argument that the K&R C "is" an utterly simple and dumb language, but if it was it's long gone and it's also irrelevant for modern hardware.
Today because C only has a single kind of reference, the raw pointer, that means if you want references at all (which you do) you need pointers, and to get decent performance from this sort of language you need pointer provenance, and so now all your reference types involve understanding compiler internals minutiae. Bad luck though, those aren't specified in the C language standard, that's a TODO item from the turn of the century. The committee agrees that C pointers do have provenance but declines to explain how that could possibly work.
auto_ptr was effectively split into std::unique_ptr and std::shared_ptr. The problem was that before C++11, there wasn't a way to distinguish between copy assignment and move assignment.
Correct. But the C++ language is not a language in which one should just say “screw it” and overload the assignment operator of all things while breaking its contract. Stuff like this is why many purists argue that operator overloading shouldn’t be a thing, because it can lead to shenanigans like this.
Circa 2004 I was in college and took a C++ class. I spent an entire week trying to get my final project working and couldn't for the life of me figure out what was wrong. It was only a few hundred lines and was like an employee record type demo. I spent about 3 hours one on one with the professor trying to figure it out. I remember that removing the auto_ptr stuff and using regular pointers would make it work (because the problem has to be with the pointer stuff right?), but part of the requirements was that I had to use auto_ptr because it was safer or whatever.
We tried compiling it on different systems and nothing would get it to work. He ended up giving me a C on the project admitting "it should work, but that doesn't cut it in the business world" or something to that effect which really pissed me off.
I just had a chat with GPT about this and that was almost certainly what was causing my program to segfault.
I don't know. Bjarne's has been saying for 40 years that C++ will eventually have opt-in garbage collectors written for it, but after all this time I don't think one exists yet unless you count C++/CLI.
For example, the 1987 edition of "The C++ Programming Language" (only 328 pages, including the index!) explains how the user can handle `new` failures with `set_new_handler` to "plug in" a garbage collection function that frees up memory and handles the failure.
And section 10.7 of "The Design and Evolution of C++", is titled "Automatic Garbage Collection", and covers in depth his reasons for not including a garbage collector, and explains a bit about how a plugin automatic collector might work. The TL;DR is that the hardware of the time was too limited and the performance overhead would have killed C++'s chances in its target market. He also posits that memory leaks "are quite acceptable" in many applications because most don't have to run forever and aren't "foundation libraries', but he's probably changed his mind on that by now.
So while a much older date is probably appropriate, maybe 20-30 years ago, we can at least mark this (2022) until somebody justifies a particular previous date.
This hides memory allocations altogether. As long as the open/close functions are paired up, it gives me confidence that there are no inadvertent memory leaks. Using small functions eases eyeballing the couplings.
For C++, developing a unit test framework based on Catch2 and ASAN that tracks new/delete invocations is rather powerful. You can even set it up to discount false positives from static allocations. When the unit tests exercise the classes, you get memory leak detection for free.
(I don't mind down votes, but at least reply with what you don't like about this approach, and perhaps suggest a newer approach that we can learn from; contribute to the conversation, please.)
> As long as the open/close functions are paired up
Let me stop you right there. I did not downvote you, but I bet that's why others did. If humans were capable of correctly pairing open/close, new/delete, malloc/free, then we could've called C's memory management "good enough" and stopped there. Decades of experience show that humans are completely incapable of doing this at any scale. Small teams can do it for small projects for a small period of time. Large teams on large projects over long eras just can't.
If the advice for avoiding resource errors includes "all the programmer has to remember is...", then forget it. It's not happening. Thus the appeal of GC languages that do this for the programmer, and newer compiled languages like Rust that handle resource cleanup by default unless you deliberately go out of your way to confuse them.
Good arguments. But for some reason there are still strange people, who prefer calling malloc/free (or new/delete or something similar) manually. Some of them even invent languages with manual-only memory management (like Zig or Odin).
One reason is that c++ still hasn't gotten 'trivial relocatability' right - i.e being able to memcpy/memmove and not have to call constructors/destructors when growing your vector class.
Actually, issues with non-trivial moves and relocations are specific only for C++. Some other languages (notably Rust and Swift) don't have such issues, but still have nice automatic memory management via destructors and containers atop of it.
C++ compilers optimize-out empty destructor calls and sometimes even replace calls to move constructors/move assignment operators via memcpy. But it's unfortunately not guaranteed in all cases due to constrains of the C++ object/memory model designed initially without proper move semantics.
I'm no fan of Odin especially, but I'd expect that one obvious defence they'd offer is that this code potentially wastes a lot of resources and if you were writing in their language you'd more likely go "Wait, that seems like a bad idea..." and produce better code.
cat += *p+"+";
Feels very cheap because it was so few keystrokes, but what it's actually doing is:
1. Making a brand new std::string with the same text inside it as `p` but one longer so as to contain an extra plus symbol. Let's call this temporary string `tmp`
2. Paste that whole string on the end of the string named `cat`
3. Destroy `tmp` freeing the associated allocation if there is one
Now, C++ isn't a complete trash fire, the `cat` std::string is† an amortized constant time allocated growable array under the hood. Not to the same extent as Rust's String (which literally is Vec<u8> inside) but morally that's what is going on, so the appends to `cat` aren't a big performance disaster. But we are making a new string, which means potentially allocating, each time around the loop, and that's the exact sort of costly perf leak that a Zig or Odin programmer would notice here.
† All modern C++ std::string implementations use a crap short string optimisation, the most important thing this is doing - which is the big win for C++ is they can store their empty value, a single zero byte, without allocating but they can all store a few bytes of actual text before allocating. This might matter for your input strings if they are fairly short like "Bjarne" "Stroustrup" and "Fool" but it can't do "Disestablishmentarianism".
I agree, having nice containers with automatic memory management allows such problems. But this code still works as intended, but it has suboptimal performance. But I think, that it's still better to use an approach allowing such performance issues, rather then bugs specific for manual memory management (memory leaks, use-after-free errors, spatial access errors).
And it's still possible to improve performance here without returning to manual memory management. Just replace it with something like this:
cat += *p;
cat += "+";
Now no temporary string is created and thrown away, only cat performs memory allocations under the hood.
No need to insult people just because you don’t understand other strategies for reducing the amount of lifetimes to track and consolidating deallocations by using memory arenas.
You can use some arena implementation in C++ too. But only when you need such an approach. If you don't care - just use std::string, std::vector or something similar.
The C++ standard library interface is broken regarding its abstraction of allocation (according to its authors). Therefore you in fact can’t just use arenas in C++ without giving up on large parts of its standard library and becoming incompatible with other code. The languages whose users you call strange don’t have this issue.
I think it's a reasonable price to have your own containers (vector/unique_ptr replacements), if you wish to use a non-standard approach for memory allocation. Many people do this, like Qt with QVector.
But do you really need arenas? Does doing allocations in a traditional way creates a bottleneck in your specific use-case? Or you just want to justify broad manual memory management (with its bugs and secure vulnerabilities) in hope to gain (or not) a tiny amount of extra performance?
Respectfully, you do not seem to understand this topic well. If the C++ standard library design abstracted memory allocation correctly, you’d still be able to use containers and algorithms and not even notice that their allocation strategy uses the heap, stack, a bump allocator, a pool, etc
Would that even work? If you use std::vector within an arena, you allocate twice as much memory as you need to, since the old arrays can't be deallocated. You might want to use std::deque instead. Memory management is not independent from data structures - std allocators smell to me like another failed experiment, like auto_ptr mentioned in another comment.
Also, with allocators you can replace new and delete, but you can't implement anything that doesn't fit into the new/delete paradigm, like any allocation strategy that requires moving objects.
I understand how C++ standard containers are designed. They are designed in such a way, so that they don't store a pointer to an allocator instance inside. This allows to save memory in the most cases (where the standard allocator is used). Overriding an allocator is possible, but only with some another global allocator (via a template argument), with no possibility to specify a per-object allocator.
As I know in newer C++ standards there is something which allows to workaround this issue. Or at least there are proposals for this.
Each container stores an allocator instance inside, not a pointer to one. If you need shared state, it's up to you to make each allocator instance contain a pointer to the shared state.
Respectfully, your way out of date with your understanding of this topic.
C++ added polymorphic memory allocators in C++17 along with polymorphic versions of all standard library containers under the std::pmr namespace, so that you have std::pmr::vector, std::pmr::map, etc... all of which fully abstract out the details of memory allocation.
Use-after-free bugs are still possible, if an allocated memory chunk outlives the arena instance used for it. With C++-style owning containers such kind of errors is possible too, but it's not so frequent.
But arenas can't be used in any case. They are suitable only if large amounts of allocations take place once and need to be deallocated all at once. If reallocation of freeing of individual memory chunks is needed, arenas can't be used, so, it's needed to manage each allocation individually, for which containers is a better choice compared to manual memory management.
1) An allocated memory chunk cannot outlive its arena (leaks are impossible). You probably mean a stale reference? The arena is put at such a level in the memory hierarchy that this bug becomes impossible. The bug here would be that the allocation was done in the wrong arena.
In C this would be avoided by putting temporary arenas in a local function scope by passing them as parameters. Fool proof references require C++ smart pointers. This is one example of you mixing concepts. Smart pointers/containers can still be used with arenas.
2) You mix up arenas and bump allocators. An arena can also use a pool allocator for example. Arena refers to the concept of scoping blocks of allocations.
3) Individual deallocations and arenas are not exclusive, for example using pools. But even with bump allocators free lists are a thing (and linked lists are more attractive in bump allocators because of locality).
>But arenas can't be used in any case. They are suitable only if large amounts of allocations take place once and need to be deallocated all at once. If reallocation of freeing of individual memory chunks is needed, arenas can't be used
It's fairly straightforward to compose memory arenas with a pool allocator in these circumstances.
Since C++11 it is permissible to write stateful memory allocators including arena based memory allocators. You can even write memory allocators that are tied to a specific object, so called sticky allocators.
> As early as 1981, I pointed out that by reducing the number of objects that I had to keep track of explicitly from many tens of thousands to a few dozens, I had reduced the intellectual effort needed to get the program right from a Herculean task to something manageable, or even easy.
Fast forward a few decades, and we're still very much on this journey of finding the right abstractions/interfaces/libraries/languages. I feel like there must be a complexity equivalent to Parkinson's law: complexity expands to fill the space left in between abstractions.
I think more people need to see this. This is how the creator of C++ thinks we should be writing code. This is what he thinks code should look like. To split a string by whitespace we should use `while (cin >> s)`. We should have a `typedef` in the middle of functions. Iterations should use `.begin()` and `.end()` everywhere. There might even be a bug with a trailing "+" appearing in the output?
Imagine if this was a new language that the dev community was seeing for the first time. It's hard to imagine it gaining much traction.
If you want a laugh, Google a tutorial for how to read a file. You should also know that all the tutorials are wrong, because they fail to handle at least one footgun or another.
There is no “modern” alternative. If you read Reddit threads, C++ programmers actually believe that it’s a reasonable file reading API.
Most companies that I’ve worked at have just implemented our own on top of the OS syscalls. Which is annoying because it requires at least a Windows and UNIX variant.
Look, I like C++. I’ve been programming in it for years. But some of the stereotypes around C++ programmers are true. I still occasionally run into design decisions so untethered from reality that it still shocks me after all these years.
QFile is mostly fine (it's probably not the fastest, but not slow), and QString is pretty comfy for all kinds of string manipulation :)
C++ has a filesystem API? TIL, never used it.
The FAQ was mostly written before C++11 with some updates since then. I don't think he is rewriting every code snippet to match modern styles. It is an enormous FAQ and not meant as an introduction to the language.
> Imagine if this was a new language that the dev community was seeing for the first time. It's hard to imagine it gaining much traction.
But it's not a new language. It's backwards compatible with C.
So "iterators" behave the same as pointers, since that's how you'd iterate through an array. You can add and subtract, then pass them to other functions.
You can't just have a function that returns a vector of strings, because that function would do an allocation. When is it deallocated? Before unique_ptr (the guide was written before), it'd be the caller's responsibility to manually do so.
Meaning you have to assign the output of that function to a variable every single time and manually remember to deallocated it or you get a memory leak.
C avoids this with `strtok` by destructively modifying the string in place. This is arguably worse.
If you were designing a new, non-GC, language, you'd have good ownership semantics and not allow pointer arithmetic. That'd be Rust.
Strange to see std::sort(), auto_ptr and RAII on the same page, when that combination was always broken.
I actually don't know (at least I don't think I do) the full story here, can you elaborate?
auto_ptr was so misdesigned, they erased it from the language in a later standard. The problem was that the assignment operator “T x = y;” is supposed to have copy semantics, but auto_ptr gave it move semantics by overwriting the source to destroy that reference. This broke all sorts of code that rightfully assumed assignment is done by copying. Therefore all kinda of algorithms like sort broke, as well as resource management. Suffice to say I am personally not a fan of C++ language design or Stroustrup’s opinions on programming.
Me neither.
I have some very smart friends who think it's the perfect language, but I kind of prefer almost every language that has come out after C++. I feel like the language adds some very strange semantics in some very strange places that can be hard to reason about until you've spent a lot of time with the language. This wouldn't necessarily be so bad if not for the fact that most people who write C++ have not spent sufficient time to understand it (and I consider myself in that group, though I don't write C++).
I have mixed feelings on D, but I'm very grateful that Rust came along. Rust is arguably even more complicated than C++, but the good thing is that getting a lot of these complications wrong will simply not allow your code to compile. It's a huge pain in the ass at first but I've become grateful for it.
I still write C very occasionally, but Rust has supplanted like 95% of jobs that were formerly C. I still really need to play with Zig.
Sounds like you would appreciate this talk: https://m.youtube.com/watch?v=wo84LFzx5nI&pp=ygUSVGhlIGJpZyB...
Personally I use C or something, anything other than C++ really, if I need something more ergonomic or provably correct. Many excuse C++’s design history with “they didn’t know better”, but the oft forgotten history explained in that video shows otherwise.
I haven't played with Zig yet, but it does look like what I want C++ to be; something unapologetically low level but given some high-level constructs for safety and ergonomics. I haven't had a huge impetus to learn it because Rust has been happily occupying that space for me but it certainly can't hurt to play with Zig.
I should write an Ffmpeg codec with it or something this weekend to try it out.
I've been having fun trying out Zig recently; would recommend. FYI that they released a big batch of API changes in April (version 0.16.0) so be mindful of learning from old resources.
Would you say Rust is more complicated than C++, or just harder to write? I think that complication a la C++ where there are too many features for the language's own good is a different problem than being a clean language but with higher order features that require a bit more, I don't know, focus?
I make a strong distinction between complexity, the inherent amount of moving parts in a thing, versus complication, the amount of frippery involved in dealing with the complexity.
Rust is complex. It's solving complex problems. It's not complicated, though. There's not much you could remove without creating leaky abstractions.
In my opinion, C++ is equally complex. It's solving the same kinds of problems as Rust (although it'd be fairer to say "Rust is solving the same problems as C++"). However, it's hella complicated. There's a vast number of twists and turns to keep in mind if you want to use it, and most of them are things you could not have anticipated by reasoning about it from first principles.
If you took the design goals of Rust, and reinvented it from scratch, it'd probably end up looking a lot like Rust. If you were to reinvent C++ from scratch, I bet it wouldn't remotely resemble modern C++. If anything, I bet it would also end up looking like Rust, and the fractal of powerful footguns would be left on the cutting room floor because "that's insane, there's no way anyone would want that".
I think there are certainly different complications in Rust than C++. The RAII can be roughly the same in the naive sense, but there's having to worry about Send and Sync and Pin and fighting with the borrow checker and all that fun stuff.
Gauging how "complicated" a language is somewhat subjective, so it's kind of hard for me to give a straightforward answer. I think it's certainly easier to be (some definition of) productive with C++ than with Rust. I feel like to do anything even remotely non-trivial with Rust, you kind of have to understand everything, because if you don't do it in the "Rust way", it often won't compile. I think this is a good thing, but it does make it harder to get started.
C++ has a lot less consistency and (kind of) more features, and lots of strange semantics to go with those features, and so if people actually use them it can get confusing and hard to read pretty quickly.
My knowledge is a bit out of date, to be clear; previously whenever I need something in the C++ domain, I could fairly easily just reach for C and use that instead. Now Rust is available and I think overall better (though I do sometimes miss how utterly simple and dumb C is).
> having to worry about Send and Sync and Pin and fighting with the borrow checker and all that fun stuff.
To be fair, the alternative to having to worry about Send/Sync/Pin is not "not worrying about Send/Sync/Pin". It's having to worry about correctly enforcing the constraints they describe on your own, without any kind of mechanical help. E.g., not moving data to another thread that shouldn't be and not accessing data from multiple threads that shouldn't be. This stuff is intrinsic.
In this sense the Rust mental model is simpler, because failing to uphold these constraints is no longer "your fault", it's Rust's fault.
I don't disagree; I was coming from Java most recently, and a lot of the equivalents of "Sync" and "Send" were just mental notes I was making myself, and I wasn't really used to it being encoded into the type system.
> have to understand everything, because if you don't do it in the "Rust way", it often won't compile
I confess I haven't dug into it much yet, but this reminds me of how Haskell was. By the time you got a program to compile your project was more or less done.
In some senses I actually find Rust a bit harder than Haskell. With Haskell the types are immutable and as such they can be happily shared everywhere. That required basically no rewiring in my brain.
With Rust, I had to get used to single ownership or explicit cloning. There's an argument that this is "better", but I found it a bit harder to learn.
Rust is similar. I don't have a link handy, but I've read a few articles noting experience with programming in rust where a program that compiles works. Provided the logic is correct, of course.
> I do sometimes miss how utterly simple and dumb C is
You can make an argument that the K&R C "is" an utterly simple and dumb language, but if it was it's long gone and it's also irrelevant for modern hardware.
Today because C only has a single kind of reference, the raw pointer, that means if you want references at all (which you do) you need pointers, and to get decent performance from this sort of language you need pointer provenance, and so now all your reference types involve understanding compiler internals minutiae. Bad luck though, those aren't specified in the C language standard, that's a TODO item from the turn of the century. The committee agrees that C pointers do have provenance but declines to explain how that could possibly work.
auto_ptr was effectively split into std::unique_ptr and std::shared_ptr. The problem was that before C++11, there wasn't a way to distinguish between copy assignment and move assignment.
Correct. But the C++ language is not a language in which one should just say “screw it” and overload the assignment operator of all things while breaking its contract. Stuff like this is why many purists argue that operator overloading shouldn’t be a thing, because it can lead to shenanigans like this.
On the contrary, C++ lets you try such things, discover that they don't work well, and then undo them.
Holy shit, I didn't know that.
Circa 2004 I was in college and took a C++ class. I spent an entire week trying to get my final project working and couldn't for the life of me figure out what was wrong. It was only a few hundred lines and was like an employee record type demo. I spent about 3 hours one on one with the professor trying to figure it out. I remember that removing the auto_ptr stuff and using regular pointers would make it work (because the problem has to be with the pointer stuff right?), but part of the requirements was that I had to use auto_ptr because it was safer or whatever.
We tried compiling it on different systems and nothing would get it to work. He ended up giving me a C on the project admitting "it should work, but that doesn't cut it in the business world" or something to that effect which really pissed me off.
I just had a chat with GPT about this and that was almost certainly what was causing my program to segfault.
std::auto_ptr<int> a(new int(5));
std::auto_ptr<int> b = a; // a becomes null
Wild.
auto_ptr is not used in the example that uses sort(), so "on the same page" is doing a bit of lifting here.
He's using auto_ptr to demonstrate RAII, which is fine. I would assume that the use of auto_ptr indicates that the example was written some time ago.
> How do I deal with memory leaks? > plug in a garbage collector.
Love it!
I don't know. Bjarne's has been saying for 40 years that C++ will eventually have opt-in garbage collectors written for it, but after all this time I don't think one exists yet unless you count C++/CLI.
For example, the 1987 edition of "The C++ Programming Language" (only 328 pages, including the index!) explains how the user can handle `new` failures with `set_new_handler` to "plug in" a garbage collection function that frees up memory and handles the failure.
And section 10.7 of "The Design and Evolution of C++", is titled "Automatic Garbage Collection", and covers in depth his reasons for not including a garbage collector, and explains a bit about how a plugin automatic collector might work. The TL;DR is that the hardware of the time was too limited and the performance overhead would have killed C++'s chances in its target market. He also posits that memory leaks "are quite acceptable" in many applications because most don't have to run forever and aren't "foundation libraries', but he's probably changed his mind on that by now.
> Modified February 26, 2022
So while a much older date is probably appropriate, maybe 20-30 years ago, we can at least mark this (2022) until somebody justifies a particular previous date.
It at least predates 2012, since the same text is seen on the this page's first archive on archive.org.
Thanks. I modified the title.
What was the last time when Bjarne Stroustrup pushed any code to production?
In raw C, I like to use the "open"/"close" metaphor and when developing, invoke the routes in parallel. For example:
https://repo.autonoma.ca/repo/mandelbrot/blob/HEAD/main.c
When writing:
I will immediately write below it: This hides memory allocations altogether. As long as the open/close functions are paired up, it gives me confidence that there are no inadvertent memory leaks. Using small functions eases eyeballing the couplings.For C++, developing a unit test framework based on Catch2 and ASAN that tracks new/delete invocations is rather powerful. You can even set it up to discount false positives from static allocations. When the unit tests exercise the classes, you get memory leak detection for free.
(I don't mind down votes, but at least reply with what you don't like about this approach, and perhaps suggest a newer approach that we can learn from; contribute to the conversation, please.)
> As long as the open/close functions are paired up
Let me stop you right there. I did not downvote you, but I bet that's why others did. If humans were capable of correctly pairing open/close, new/delete, malloc/free, then we could've called C's memory management "good enough" and stopped there. Decades of experience show that humans are completely incapable of doing this at any scale. Small teams can do it for small projects for a small period of time. Large teams on large projects over long eras just can't.
If the advice for avoiding resource errors includes "all the programmer has to remember is...", then forget it. It's not happening. Thus the appeal of GC languages that do this for the programmer, and newer compiled languages like Rust that handle resource cleanup by default unless you deliberately go out of your way to confuse them.
Remarkably similar to dykstra on debugging.
Good arguments. But for some reason there are still strange people, who prefer calling malloc/free (or new/delete or something similar) manually. Some of them even invent languages with manual-only memory management (like Zig or Odin).
One reason is that c++ still hasn't gotten 'trivial relocatability' right - i.e being able to memcpy/memmove and not have to call constructors/destructors when growing your vector class.
Actually, issues with non-trivial moves and relocations are specific only for C++. Some other languages (notably Rust and Swift) don't have such issues, but still have nice automatic memory management via destructors and containers atop of it.
C++ compilers optimize-out empty destructor calls and sometimes even replace calls to move constructors/move assignment operators via memcpy. But it's unfortunately not guaranteed in all cases due to constrains of the C++ object/memory model designed initially without proper move semantics.
std::is_trivially_copyable says hi
I'm no fan of Odin especially, but I'd expect that one obvious defence they'd offer is that this code potentially wastes a lot of resources and if you were writing in their language you'd more likely go "Wait, that seems like a bad idea..." and produce better code.
Feels very cheap because it was so few keystrokes, but what it's actually doing is:1. Making a brand new std::string with the same text inside it as `p` but one longer so as to contain an extra plus symbol. Let's call this temporary string `tmp`
2. Paste that whole string on the end of the string named `cat`
3. Destroy `tmp` freeing the associated allocation if there is one
Now, C++ isn't a complete trash fire, the `cat` std::string is† an amortized constant time allocated growable array under the hood. Not to the same extent as Rust's String (which literally is Vec<u8> inside) but morally that's what is going on, so the appends to `cat` aren't a big performance disaster. But we are making a new string, which means potentially allocating, each time around the loop, and that's the exact sort of costly perf leak that a Zig or Odin programmer would notice here.
† All modern C++ std::string implementations use a crap short string optimisation, the most important thing this is doing - which is the big win for C++ is they can store their empty value, a single zero byte, without allocating but they can all store a few bytes of actual text before allocating. This might matter for your input strings if they are fairly short like "Bjarne" "Stroustrup" and "Fool" but it can't do "Disestablishmentarianism".
I agree, having nice containers with automatic memory management allows such problems. But this code still works as intended, but it has suboptimal performance. But I think, that it's still better to use an approach allowing such performance issues, rather then bugs specific for manual memory management (memory leaks, use-after-free errors, spatial access errors).
And it's still possible to improve performance here without returning to manual memory management. Just replace it with something like this:
Now no temporary string is created and thrown away, only cat performs memory allocations under the hood.No need to insult people just because you don’t understand other strategies for reducing the amount of lifetimes to track and consolidating deallocations by using memory arenas.
You can use some arena implementation in C++ too. But only when you need such an approach. If you don't care - just use std::string, std::vector or something similar.
The C++ standard library interface is broken regarding its abstraction of allocation (according to its authors). Therefore you in fact can’t just use arenas in C++ without giving up on large parts of its standard library and becoming incompatible with other code. The languages whose users you call strange don’t have this issue.
I think it's a reasonable price to have your own containers (vector/unique_ptr replacements), if you wish to use a non-standard approach for memory allocation. Many people do this, like Qt with QVector.
But do you really need arenas? Does doing allocations in a traditional way creates a bottleneck in your specific use-case? Or you just want to justify broad manual memory management (with its bugs and secure vulnerabilities) in hope to gain (or not) a tiny amount of extra performance?
Respectfully, you do not seem to understand this topic well. If the C++ standard library design abstracted memory allocation correctly, you’d still be able to use containers and algorithms and not even notice that their allocation strategy uses the heap, stack, a bump allocator, a pool, etc
Would that even work? If you use std::vector within an arena, you allocate twice as much memory as you need to, since the old arrays can't be deallocated. You might want to use std::deque instead. Memory management is not independent from data structures - std allocators smell to me like another failed experiment, like auto_ptr mentioned in another comment.
Also, with allocators you can replace new and delete, but you can't implement anything that doesn't fit into the new/delete paradigm, like any allocation strategy that requires moving objects.
I understand how C++ standard containers are designed. They are designed in such a way, so that they don't store a pointer to an allocator instance inside. This allows to save memory in the most cases (where the standard allocator is used). Overriding an allocator is possible, but only with some another global allocator (via a template argument), with no possibility to specify a per-object allocator.
As I know in newer C++ standards there is something which allows to workaround this issue. Or at least there are proposals for this.
Each container stores an allocator instance inside, not a pointer to one. If you need shared state, it's up to you to make each allocator instance contain a pointer to the shared state.
Respectfully, your way out of date with your understanding of this topic.
C++ added polymorphic memory allocators in C++17 along with polymorphic versions of all standard library containers under the std::pmr namespace, so that you have std::pmr::vector, std::pmr::map, etc... all of which fully abstract out the details of memory allocation.
Isn't using Arena just simplifying memory management, thereby reducing the amount of bugs as well?
Use-after-free bugs are still possible, if an allocated memory chunk outlives the arena instance used for it. With C++-style owning containers such kind of errors is possible too, but it's not so frequent.
But arenas can't be used in any case. They are suitable only if large amounts of allocations take place once and need to be deallocated all at once. If reallocation of freeing of individual memory chunks is needed, arenas can't be used, so, it's needed to manage each allocation individually, for which containers is a better choice compared to manual memory management.
All of this is false.
1) An allocated memory chunk cannot outlive its arena (leaks are impossible). You probably mean a stale reference? The arena is put at such a level in the memory hierarchy that this bug becomes impossible. The bug here would be that the allocation was done in the wrong arena. In C this would be avoided by putting temporary arenas in a local function scope by passing them as parameters. Fool proof references require C++ smart pointers. This is one example of you mixing concepts. Smart pointers/containers can still be used with arenas.
2) You mix up arenas and bump allocators. An arena can also use a pool allocator for example. Arena refers to the concept of scoping blocks of allocations.
3) Individual deallocations and arenas are not exclusive, for example using pools. But even with bump allocators free lists are a thing (and linked lists are more attractive in bump allocators because of locality).
1) In practice, this is not true, especially if you implement unwinding in your arena.
You probably don't want to have an Arena in main, and you do all of your allocations from there, for example. That "just" leaks everything.
Here's a classic Arena-with-rewind bug:
Thanks. This is a good illustration of how data structures aren't cleanly separable from memory management strategies.
>But arenas can't be used in any case. They are suitable only if large amounts of allocations take place once and need to be deallocated all at once. If reallocation of freeing of individual memory chunks is needed, arenas can't be used
It's fairly straightforward to compose memory arenas with a pool allocator in these circumstances.
Your knowledge is outdated by about 15 years.
Since C++11 it is permissible to write stateful memory allocators including arena based memory allocators. You can even write memory allocators that are tied to a specific object, so called sticky allocators.
This is how I do it: Claude, how do I deal with memory leaks?