3 ms·
It depends on what you are working on, and then even "not wrong by default" might be... wrong. For low level code, destructors/RAII are much harder to notice t
by anyfoo 4y ago
It depends on what you are working on, and then even "not wrong by default" might be... wrong.
For low level code, destructors/RAII are much harder to notice than a very explicit defer. Even the "kernel C" style "goto done;" to a unified exit point (sometimes multiple labels that fall into each other to clean up according to how far you got) wins out by explicitly telling you what will happen.
If you are working on projects where you often spend much more time reading code than subsequently writing code, and if the thing you are working on is not a high level application where the meat is in some business logic, then anything that reduces readability in the sense of "I can see everything that is going on" is very bad.
In low level code, the potential for icky problems is also much higher: You don't want to find out that some stray destructor did something that should not have happened in e.g. an interrupt context, or while the current thread's memory management is in an intermediate state. That is not correct by default, it is "wrong and you didn't even tell me".
In high level application software development, e.g. CRUD SaaS or a smartphone app, the opposite may be the case, and decluttering might well win out.
- kllrnohj 4y ago> then anything that reduces readability in the sense of "I can see everything that is going on" is very bad. Nonsense. So called "kernel C" still uses functions, and lots of them, scattered over thousands of files. You never "see everything that is going on." And "kernel C" also uses function pointers and macros, further preventing you from "seeing everything that is going on." Having written and read lots and lots of C++, RAII being "hidden" is hugely overblown by the diehard C crowd. It's really not hidden at all, first of all (it's often right there in the type - pretty hard to forget that unique_ptr is going to call delete, for example). And second it goes so so far in preventing resource leak bugs, which is also something kernels and low level code care a lot about. In fact, they almost always care far more about runtime correctness on this than readibility. The benefits RAII provide here so massively outweigh the "hidden" concerns it's almost laughable. > You don't want to find out that some stray destructor did something that should not have happened in e.g. an interrupt context, or while the current thread's memory management is in an intermediate state. That is not correct by default, it is "wrong and you didn't even tell me". RAII isn't just randomly injected. It's governed by the types you use. If a given type cannot be safely cleaned up in a given context, then it probably also wasn't safe to get in that context in the first place. Which the type should be communicating. So no it's not "wrong by default" since the initial (explicit!) acquisition was the "wrong" part, not the cleanup. Regardless, it's not like C helps here at all. You still have to call the cleanup function for your given data, and that cleanup function may change without your interrupt handler knowing about that change.
- anyfoo 4y ago> Having written and read lots and lots of C++, RAII being "hidden" is hugely overblown by the diehard C crowd. I work on low level projects which use both at the same time. > If a given type cannot be safely cleaned up in a given context, then it probably also wasn't safe to get in that context in the first place. Not true. As a quick example, the destructor might try to take some statistics, which might not be safe in an interrupt context, or when a thread is (internally) in an intermediate state. There might also be multiple ways to get rid of the type. This was an "obvious" example, the ones that bite you are the non-obvious ones of course. And yes, I have anticipated both the "but you still call functions in C" and the "functions may change" comment. True, but you call those functions explicitly, at a very explicit point (which you can also surround with other code), and anyone who works on a cleanup/destructor-like function can very easily figure out all call sites. My point is precisely that you tend to run into these icky issues when deep in the bowels of a system, not when writing a smartphone app. > And second it goes so so far in preventing resource leak bugs, which is also something kernels and low level code care a lot about. Kernels do care about that, true. But if you exchange resource leak bugs for invalid state, which translates much more happily into security bugs, you are definitely worse off.
- kllrnohj 4y ago> Not true. As a quick example, the destructor might try to take some statistics, which might not be safe in an interrupt context, or when a thread is (internally) in an intermediate state. Which is a problem you also have in C. If someone is going and adding, say, lock duration statistics they aren't auditing all the C call sites just like they wouldn't be doing that with C++ destructors. Which you can search for, fwiw. You seem to be under an incorrect assumption that you can't find those usages. You can. > There might also be multiple ways to get rid of the type. This is not incompatible with RIAA. See for example unique_ptr's optional destructor. Of course, you're always free to just not use RIAA in those specific contexts.
- anyfoo 4y ago> Which is a problem you also have in C. Personal opinion will differ, but after reading and working on both C and RAII-heavy C++ low level code for many years (though I don't do the C++ one anymore), the low level C code just feels like fresh air when auditing.[1] It's all so explicit. For high level, as mentioned initially, I feel the opposite, as the boilerplate gets in the way. > Of course, you're always free to just not use RIAA in those specific contexts. That's my point, is it not? RAII in high level code, not in low level code. I did not claim they have to be in different code bases. And I am really not afraid of abstraction, my favorite language is Haskell. But that's so high level, and so far from the metal, that every function looks pure, and every value is lazy by default. [1] Personally, I think Rust would be my choice.
- _gabe_ 4y agoI don't get why you're getting down voted. This seems like a well balanced approach. I feel like so many programmers these days focus on "pretty" code and not readable code. Hidden complexity is hidden complexity. RAII hides complexity, and it is not like a function call as another user tried to say. RAII implicitly happens at the end of an object's lifetime. In other words, there is no code that shows where or if this happens. Unless you look at the documentation or the code for the object you're using and manually check and see if it actually has a non default destructor. Whereas you can see exactly where a function is called and then inspect the details if you want. I don't get why programmers so vehemently oppose a mix of good ideas. Why does everything _have_ to be RAII? Or everything _has_ to be OOP? I think we're finally making strides away from this mentality, but more people need to be thinking about the benefits of mixing several different styles. There are pros and cons to RAII and there are pros and cons to defer. There is no silver bullet. I hope people quit pretending that there is a silver bullet. And instead embrace the good ideas that come out of different styles of coding instead of shoehorning everything into the patterns that we're already used to.