4

Regarding the order of destruction of static variables in C++, are there any guarantees about the lifetime of static objects with respect to their static member variables?

For example, if I had something like this (insanely simplified example for demonstration purposes only):

class Object {
    static std::vector< Object * > all_objects;

public
    Object() {
        all_objects.push_back( this );
    }

    ~Object() {
        all_objects.erase(
          std::remove(all_objects.begin(), all_objects.end(), this), 
          all_objects.end());
    }
};

Would this be "safe" with respect to static Objects in different compilation units? That is, is there any guarantee that the all_objects member variable will stick around at least as long as any valid Object, or could there be an issue where all_objects is destroyed prior to the last Object instance?

And does the answer change if the code is being used as a library (say within Python) rather than as a standalone program with its own main()?

R.M.
  • 3,461
  • 1
  • 21
  • 41

4 Answers4

1

Would this be "safe" with respect to static Objects in different compilation units?

No, it is not safe.

This is an example of when it would not be safe since the relative order of static data initialization is not guaranteed. There are some platform specific ways to achieve this though.

See the FAQ for techniques to work around this static initialisation fiasco. The technique basically hides the static member in a function and it is then initialised on first use.

It could be made to be safe so long as the objects added to the static member are managed appropriately, i.e. not static and they are not left dangling somewhere or in some undefined state and suffer no other undefined behaviour.

And does the answer change if the code is being used as a library (say within Python) rather than as a standalone program with its own main()?

I don't believe this would be defined by the standard other than it being implementation defined. As far as I know, no, given the popular implementations, the platforms and their ABI etc. the answer would not change.

Niall
  • 30,036
  • 10
  • 99
  • 142
1

Would this be "safe" with respect to static Objects in different compilation units?

It is not safe at initialization time. There is no guarantee that all_objects will be initialized at the time a static object in a compilation unit is constructed.

I am not clear on the order of termination. My guess is that destruction happens in the reverse order of construction. If construction/initialization is not safe, destruction is likely to be unsafe too.

One way to make it safe at initialization time is wrap all_objects in a function.

class Object {
    static std::vector<Object *>& get_all_objects();

public
    Object() {
        get_all_objects().push_back( this );
    }

    ~Object() {
       std::vector<Object *>& all_objects = get_all_objects();
       all_objects.erase(
          std::remove(all_objects.begin(), all_objects.end(), this), 
          all_objects.end());
    }
};

std::vector<Object *>& Object::get_all_objects()
{
    static std::vector<Object *> all_objects;
    return all_objects;
}

This is what the C++11 Standard (3.6.3/1) has to say about destruction of objects with static storage duration.

If the completion of the constructor or dynamic initialization of an object with static storage duration is sequenced before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first.

Given that, the above approach is safe for destruction. all_objects will be destructed only after the last Object will be destructed.

R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • What's the destruction order rules for function-level static variables versus global (or class) level static variables? Are we sure `all_objects` will stick around until all the static Objects disappear? – R.M. Sep 16 '16 at 21:43
  • Does the "completion of the constructor" bit change things? e.g. http://stackoverflow.com/a/335746/3022952 indicates that, as you called the get_all_objects() function in the constructor for Object, then all_objects variable will complete its constructor prior to the completion of even the first Object's constructor, meaning that the destructor for all_objects will only be called after the destructor for *all* Objects completes. Or am I interpreting things wrong? – R.M. Sep 16 '16 at 22:04
1

Static variables really have global scope in that they are not on the stack for a function or method. So the destructors are called at the last possible time.

So in a single thread environment I can't see any problem with it. Here is a silly example but it does run. After the return statement the destructor for the two statics is called.

ob.h
class ob
{
  static int a;
  static int b;
public:
  ob()
  {
    a++;
  }
  ~ob()
  {
    b--;
  }
};

main.cpp #include ob.h;

int ob::a = 0;
int ob::b = 0;

void tt()
{
  static ob zz;
}

int main() 
{
  static ob yy;
  tt();

  {
    ob b;
  }
  ob a;

  return 1;
}

Regarding the static vars in another compilation unit, would depend on how you used it. For example if everything is inlined and the header is used in A.dll and B.dll, there would be no reference between them and you would have to initialize the static in each unit giving them a unique address. But if it was in a lib or dll where it was exported you would be using the same memory address. I've seen a problem with this before where we had two versions of the same class. Object A was 1.0 and object B was 1.2. They were not exported directly but were used in functions that were exported. The wrong destructor was called for the objects. That was a really poor coding choice and was changed. In a multi-threaded build it could be very bad depending on how you are using the objects. You can't know the order of destruction and you could try to access something after it was destroyed.

Overall I would say not a good practice. It will work but in a more complex situation, future changes could break things fast.

0

To complete the @Niall's answer, although the order of initialization is undefined, the order of destruction will be the inverse of the initialization order.

The only way to be sure about anything, will be through the creation of a global function with your object as a static local variable (as shown in other answers).

In that case, you will know for sure that the static object will be deleted "before" the static class member, because it was created "after" (the first time you call the global function):

class Object {
    static std::vector< Object * > all_objects;

public
    Object() {
        all_objects.push_back( this );
    }

    ~Object() {
        all_objects.erase(
          std::remove(all_objects.begin(), all_objects.end(), this), 
          all_objects.end());
    }
};

Object& static_obj()
{
     static Object obj;
     return obj;
}

std::vector< Object * > Object::all_objects; // It is created first (before main)

int main()
{
    Object& o = static_obj(); // `obj` is initialized here.
} // At the end of the program, `obj` will be destroid first.
ABu
  • 10,423
  • 6
  • 52
  • 103