0

What is the difference between functions in .dtors and functions called using atexit()?

As I understand, functions marked with the ((destructor)) attribute are located in the .dtors segment, and called after exit. Likewise, functions added using atexit(fctName) are placed in an array and also called after a normal execution end.

So why does C++ provide two distinct mechanisms here? Are there distinct things that can be done only with one? Can I only add a function dynamically using atexit()?

Also which are called first, functions in .dtors or functions added using atexit()?

zenzelezz
  • 813
  • 2
  • 10
  • 26
Rafa
  • 1,151
  • 9
  • 17
  • C++ doesn't *provide* a .dtors segment. It's an implementation detail. `destructor` attribute isn't defined by the standard either. It's an extension to the (C) language. `atexit` is defined in the C standard, so one could say that it's in C++ because it was inherited from C. – eerorika Feb 02 '15 at 15:17

3 Answers3

1

From linux man-pages atexit() is called at normal process termination, either via exit(3) or via return from the program's main().

As for .ctors / .dtors, they are called when a shared library in which they are defined is loaded / unloaded.

The order in which these will occur is quite obvious.

Tom
  • 2,481
  • 1
  • 15
  • 16
  • With regards to shared libraries... They are undefined behavior as far as the standard is concerned. But the C++ standard does require proper interleaving of destructors and the functions registered with `atexit`. In the past (I don't know if it's still the case), g++ had a few gotcha's in this respect and wasn't always conform: see `__cxa_atexit1`. – James Kanze Feb 02 '15 at 15:48
  • Would the behavior be different if the shared library call atexit() with a function doing the same thing as the code in the destructor? – Rafa Feb 02 '15 at 19:06
  • [this entry](http://stackoverflow.com/questions/10702980/atexit-function) seems to answer your question. Appears that outside the glibc implemetation, doing so would be undefined behavior. – Tom Feb 03 '15 at 07:53
  • @user3043261 Any behavior of `atexit` with shared libraries is very implementation defined. The standard doesn't recognize shared libraries. – James Kanze Feb 03 '15 at 17:48
0

C++ doesn't have .dtors. Some implementations might. It's a reasonable mechanism to keep track of destructors of global objects. As I understand it, it's a compile-time list.

atexit handlers are added at runtime, though. That means you can add functions only if and when they become necessary at runtime.

For the last part of your question (and more details), see When is a function registered with atexit() called

Community
  • 1
  • 1
MSalters
  • 173,980
  • 10
  • 155
  • 350
  • It can't be a compile time list, since the order of destruction must correspond to the inverse of the order of construction, and must interleave correctly with calls to `atexit`. – James Kanze Feb 02 '15 at 15:09
  • @JamesKanze: I don't see exactly why that's a problem? The `.dtors` list is just a compiled-in list of functions pointer, which are dynamically to some `__atexit__internal` list as the corresponding `.ctors` finish. If there are interleaving `atexit()` calls, they too would be added to that `__atexit_internal` list. Still, the point is that the list is dynamically built by merging table entries and `atexit()` arguments. – MSalters Feb 02 '15 at 15:19
  • See my response to your comment on my answer. Basically, you seem to just be considering the simple case, and ignoring local statics and initializers which themselves call `std::atexit`. And the fact that the standard requires functions registered with `atexit` and destructors of static objects to be sequence in the inverse order of the calls to `atexit` and the constructors. – James Kanze Feb 02 '15 at 15:29
  • Yes? Still seems to be easier to start with an initial compiled-in list of some form, and modify that dynamically if (and only if) needed. After all, for many programs you'd never modify that initial list. And when you do, splicing an entry in a single linked list isn't rocket science either. – MSalters Feb 02 '15 at 15:41
  • That's also a legal solution, but it can be tricky. How do you determine where to splice in? And in practice, `atexit` will be part of the C library, and won't know about this list. Consider too the possibility of Meyers singletons, where the `instance` function has a local static, will often be called from the constructors of other static objects, and whose destructor must also be spliced in the right place. – James Kanze Feb 02 '15 at 15:52
  • @JamesKanze: You'd walk the list using a pointer to each node, incrementing it each time a global ctor successfully completes. If you haven't finished the ctor list, you splice in an `atexit` handler in the middle of the dtor list. Once you've finished constructing all globals, the pointer would be to the end of the list so any future `atexit` calls would append handlers. Execution is a matter of walking the list in reverse. But indeed, if you must reuse the C `atexit` library, this isn't the most convenient way. – MSalters Feb 02 '15 at 20:58
  • I'm not saying it can't be done, but since typically `atexit` is already there... Of course, the issue becomes more complicated when dynamic loading and unloaded are taken into account: I presume that if you load a DLL, register a function in it with `atexit`, then unload the DLL, you're shooting your self in the foot. And that implementations don't necessarily respect the order required by the standard if you load and unload DLLs in arbitrary order. So it seems quite possible that an implementation uses `atexit` in the binary, and your technique in DLLs. – James Kanze Feb 03 '15 at 17:52
-1

One legal implementation of destructors of static objects is to register them with atexit when the constructor has finished. The standard requires the order to be the same as if this implementation was used. The main difference is that the destructors of static objects are destructors: they will be called automatically if the object is completely constructed, without any necessity on your part to register them. And they have a this parameter to access the object.

EDIT:

To make it perfectly clear: given

T obj;      //  where obj has static lifetime...

The compiler would generate a function:

void __destructObj()
{
    obj.~T();
}

and the following initialization code:

new (&obj) T;
std::atexit( __destructObj );

This regardless of the scope of obj; the same basic code works for both local statics and objects at namespace scope. (In the case of local objects, the compiler would also have to generate a flag and code to test it to indicate whether the object had already been initialized; it would also have to take steps to ensure thread safety.)

It is, in fact, difficult to see how the compiler could do it otherwise (although it might generate code inline to do what std::atexit does), given the ordering requirements.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
  • How would you register the `this` pointer for a destructor of a global object? Behind the scenes, there has to be some common mechanism (as the interleaving is documented behavior) but you can't simply reuse the public `atexit` API. – MSalters Feb 02 '15 at 15:21
  • @MSalters The compiler has to generate a function which doesn't take an argument, and register that. Which isn't too difficult, since it knows the address off the object. On the other hand, the ordering constraints in the standard pretty much require dynamic registration: if I have `T o1; bool b = (std::atexit( f ), true); T o2;`, the standard requires the order `~T(o2); f(); ~T(o1);`. (Why is different question. Anyone who actually writes stuff like that deserves to be shot.) – James Kanze Feb 02 '15 at 15:26
  • Seems a bit complex (the overhead of those helper functions could be fairly large) but it would work, yes. Why somebody would need this, I think the compelling argument was Andrei Alexandrescu's work on proper cleanup (Phoenix Singleton, IIRC, in Modern C++ Design). – MSalters Feb 02 '15 at 15:38
  • @MSalters It isn't simple, but I think the idea is that there should be very few objects with static storage duration. I might add that early C++ compilers didn't do this; they just generated a single function which called all of the destructors of static objects (including, in at least one case, the destructors of local statics which had never been constructed), and called functions registered with `atexit` either before any destructors or after all. The C++98 standard forbid this behavior. – James Kanze Feb 02 '15 at 15:41
  • Do you happen to know roughly when C++ compilers started supporting the C++98 standard behaviour? I looked at Cfront 2.0 and it seems that the manual _says_ that static destructions are ordered with respect to `atexit` function invocations, but the source code doesn't seem to actually implement this feature (though, since I can't figure out how to compile it, I can't be quite sure) – Brian Bi Nov 25 '21 at 16:52