8 ms·
C++ Exit-Time Destructors
- pizlonator 3y agoGreat write-up! I was just spelunking in this part of the language recently. Worth noting that global constructors and destructors are generally sucky for a bunch of reasons. Don’t use them unless you really have to. Avoid them in libraries. Reason to avoid global constructors: there’s no laziness to them; they definitely run on program start even if the global is never used. For small projects, no big deal. For large projects it means a large startup time penalty. Successful projects get big eventually, so if you’re playing the long game you want to assume you’re a big project. If you’re a library writer, it’s probable that you’ll be linked into something big. If you don’t have global ctors then your clients will thank you. Atexit anything is just pointless, IMO. The best way for a process to exit is to just exit. Trying to “destruct” your state at exit risks race conditions and weird bugs but buys you very little. Even things like flushing or syncing a file probably don’t belong in atexit since you should assume your program will run a long time so you want important stuff flushed and synced way before the exit happens. Also, global dtors are implemented by having a global ctor that does __cxa_atexit, so see previous paragraph. And as with all programming advice, your mileage may vary and some advice is best ignored. If global ctors/dtors achieve what you want and/or startup time isn’t an issue, then do what you feel, bro
- KerrAvon 3y agoYes. If you have an atexit or global destructor on a program expected to run on anything Unixlike or Windows >= NT, you’re just making things slower for the user. Unless you’re on AmigaOS, let the kernel do its job; it can release your resources much faster than you can.
- dataflow 3y ago> If you have an atexit or global destructor on a program expected to run on anything Unixlike or Windows >= NT, you’re just making things slower for the user. Unless you’re on AmigaOS, let the kernel do its job; it can release your resources much faster than you can. This is a pretty myopic view of things? There are lots of other things you might wish to do when the program terminates besides releasing resources...
- pizlonator 3y agoLike what?
- dataflow 3y agoLike https://stackoverflow.com/a/9334778 https://stackoverflow.com/a/9334778
- masklinn 3y agoOr signaling external systems, or removing lockfiles, … Anything the kernel does not do on program exit, which is most things.
- gsliepen 3y agoAt a previous job I was using destructors to ensure motors and other actuators in a telescope would shut down properly, and in the right order. This was very important, because incorrect shutdown could cause damage to equipment and potentially people. This was actually my main reason for using C++ to program the control software: the fact that constructors and destructors run deterministically, especially when something bad happens and an exception is thrown. And while the language doesn't specify in which order they run for global variables in different compilation units, you still know how they are ordered with respect to `main()`, and if you pay attention to how the linker works, you can actually know how they will all be ordered.
- pizlonator 3y agoOk but couldn’t you have done that prior to calling exit?
- dataflow 3y agoWhat problem would that have solved? Also... can't you make that argument for anything? "Avoid unique_ptr -- can't you just free the memory before exiting?"
- nmcveity 3y agoBut sometimes not fast enough. IIRC, you can wait for a HPROCESS in Win32 with the WaitForSingleObject call but that is signaled when the application code finishes _not_ when the OS has finished it's clean up. So if the process you were waiting on was an application that would write, say, "a.txt" and you wanted to wait until that process was done so that you can read "a.txt", it is possible that you fail to open that file because the OS had not released the file resource. Of course, I wouldn't put that clean up in an atexit.
- akoboldfrying 3y ago>Even things like flushing or syncing a file probably don’t belong in atexit since you should assume your program will run a long time so you want important stuff flushed and synced way before the exit happens. I definitely want buffers to be flushed to disk if the user Ctrl-C's the program. The OS only ensures that the resources are released at the OS level, but does nothing to prevent the persistent application state becoming inconsistent at the application level. The really hardcore way to do this is to structure your whole program so that all state is recorded as deltas appended to a WAL, with a background thread/process periodically merging this into "current state" -- in that case, even pulling the power cord can't leave your application in an inconsistent state. But that's a whole lot of work, and just best-effort flushing buffers on exit gets you 80% of the way there.
- gpderetta 3y agoOr you do the easy mode and use sqlite for your file format.
- gpderetta 3y agoNot much to add. Strongly agree. Destructors are to preserve internal program invariant. External invariants need transactional guaratees.
- saagarjha 3y ago> Potential race conditions: Destructors might execute during thread termination, while other threads still attempt to access the object. Examples: webkit Haven’t looked into the bug closely but I will say that WebKit compiles with thread safety on statics turned off, which means they are in a non-standard language mode with far more races.
- o11c 3y agoSide note: dynamic library unloading (and, for that manner, multiple loading of the same library; see `dlmopen`) is finicky because the language has no concept of ownership of things like function pointers and pointers to static data. It's easy to imagine a better language, though this would of course require a lot of low-level implementation work!
- dataflow 3y ago> the language has no concept of ownership of things like function pointers and pointers to static data. It's easy to imagine a better language I actually have both a hard time imagining a better language (not saying it's impossible, just that it's not trivial for me to see) and also how ownership would resolve this issue. Could you elaborate?
- lldb 3y agoI tested this recently and it definitely works as expected in c++ - destructors of static lifetime variables are called when the library is unloaded.
- rwmj 3y agoOnly in trivial cases. The problem is when another part of the program holds a function pointer into the library which has been unloaded. This can happen in particular when you combine libraries and threads: libvirt has long been linked with -Wl,-z -Wl,nodelete to avoid this: https://gitlab.com/libvirt/libvirt/-/commit/8e44e5593eb9b89fbc0b54fde15f130707a0d81e https://gitlab.com/libvirt/libvirt/-/commit/8e44e5593eb9b89f... Another case in libnbd which really demonstrates how hard this is: https://gitlab.com/nbdkit/libnbd/-/commit/368e3d0d5a8871aad5e1d3e9b96dc52f03cdfc7b https://gitlab.com/nbdkit/libnbd/-/commit/368e3d0d5a8871aad5...
- alexey-salmin 3y ago> The problem is when another part of the program holds a function pointer into the library which has been unloaded. How is this different from holding a pointer to an object being deleted?
- wyldfire 3y agoIt'd be great if we could come together and specify some of these dark corners like what should happen to static destructors and at_thread_exit handlers at dlclose time. Since it's unspecified, musl's behavior is just as correct as glibc's or BSD's or ...
- martijntje 3y agoIt's interesting to see that the complexity of c++ is so great that even the code in standards proposals contains undefined behaviour. The no_destroy class triggers UB when .get() is called, because it fails to launder the returned pointer. This is required since c++17.
- pjmlp 3y agoThe way C++ is going is no longer the language I felt in love as my next programming tool after Object Pascal (Turbo/Delphi). C++26 is going to introduce erroneous behavior. https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2795r4.html https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p27...
- maccard 3y agoThis is absolute madness, and IMO goes against everything c++ should stand for. It's a non-zero runtime overhead breaking language change, for a case that should be handled by disallowing uninitialized reads, with an opt in for undefined behaviour.
- pjmlp 3y agoThe madness is the increasing complexity, adding features into the standard without having them being tested on field (like in any other sane language), last meeting had 210 people voting in for their special features, trying to fix safety without cleaning out the features that make it impossible in first place, and so on. As for non-zero runtime overhead, that is long gone, since stuff like std::map, std::regexp and refusal from compiler vendors to break an ABI that isn't even defined on the ISO level, yet it blocks any proposal that has the side effect of breaking it.
- maccard 3y ago> adding features into the standard without having them being tested on field Could not agree more. Modules are a perfect example of this. > As for non-zero runtime overhead, that is long gone, since stuff like std::map, std::regexp I have a little sympathy for this - they're libraries that are replaceable (yet you'd think we'd have learned from map, regex, unique_ptr that getting it right first time is impossible, and _maybe_ we should start putting these things in the language), and the overhead there is opt-in. In this case, if I have the following code: char buf[SOME_VERY_LARGE_CONSTANT]; bool result = fill_buf_from_c_library(buf, sizeof(buf) / sizeof(buf[0])); assert(result); // we've guaranteed at runtime that this memory is initialized now comes with a significant performance hit with a compiler upgrade, and requires code changes to fix > refusal from compiler vendors to break an ABI that isn't even defined on the ISO level, yet it blocks any proposal that has the side effect of breaking it. And yet this is true at the same time. Madness.
- ryandrake 3y agoPrevious discussion: https://news.ycombinator.com/item?id=39737622 https://news.ycombinator.com/item?id=39737622
- worstspotgain 3y agoOn Mac and iOS, this thorny issue is made even worse by some ancient Darwin design choices. When the process is terminated, the main thread executes static destructors while the other threads are still running. This causes unpredictable exit-time crashes in (typically cross-platform) code that uses static variables with nontrivial destructors. The solution is to always terminate all threads before exiting. This requires a bit of internal infrastructure that's missing from many projects. If a cross-platform library is being used, it may start its own untracked threads. On iOS, app processes only terminate in special circumstances. When this issue yields a crash it's (currently) imperceptible to the user. However, it's still tracked in the crash statistics.
- gpderetta 3y agostd::quick_exit
- worstspotgain 3y agoSure, and on many systems there are platform-specific ways of terminating the process. However: - Executing static destructors without crashing is the ideal result. - On Mac, there are factors that make the thread-termination approach preferable, such as using cross-platform libraries or a framework like Qt. - On iOS, none of these apply, as process termination is out of your hands.
- MichaelMoser123 3y agothe article didn't mention the “static initialization order ‘fiasco’" https://isocpp.org/wiki/faq/ctors#static-init-order https://isocpp.org/wiki/faq/ctors#static-init-order If you have two static objects in two different files then good luck if one depends on the other, as the initialization order is not well defined. If both files are in the same executable then you can get around that by linking the object files in the desired order - well mostly, kind of. i have hit this problem at some point...
- josefx 3y agoThat sounds like hard mode, the easy solution is to just initialize them the first time they are used. GetInstance(){ if (!instance)instance = new Foo; return instance;}, add a lock if you need thread safety and done.
- MichaelMoser123 3y agoYes, you fix the problem by calling the initialization stuff from a single point.
- rcxdude 3y agoor just: Foo &getInstance() { static Foo instance; return instance; } Guaranteed to be threadsafe by the standard, even.
- ooterness 3y agoAs an embedded C++ developer, exit-time destructors have bit me many times. In many cases, they pull in 100+ kB of useless code that only runs on exit. I've only got 128 kiB of RAM and my program doesn't exit, but good luck convincing the compiler this is true. It's so aggravating.
- roeles 3y agoRecent tests with link time optimization give me the impression that code which is not actually called is not included (or even resolved). If you can convince the compiler that the destructor is never called, it might not include it.
- ooterness 3y agoYes, that's correct. Assuming "-ffunction-sections" is set during compilation, and "--gc-sections" is set during linking, it'll drop any unreachable code. Sadly, small oversights (e.g., exception handling for undefined virtual methods) can cause disproportionate bloat.