12

Does gcc have any guarantees about static member initialization timing, especially regarding template classes?

I want to know if I can get a hard guarantee that static members (PWrap_T<T>::p_s) will be initialized before main(), when classes are instantiated across multiple compilation units. It isn't practical to try to manually touch a symbol from each compilation unit at the start of main, but it isn't clear to me that anything else would work.

I've tested with methods like bar() in different units and always gotten the desired result, but I need to know when/if ever gcc will yank the rug out and whether it's preventable.

Furthermore, will all static members from a DSO be initialized before a library finishes loading?

#include <iostream>
#include <deque>

struct P;
inline std::deque<P *> &ps() { static std::deque<P *> d; return d; }
void dump();

struct P {
  P(int id, char const *i) : id_(id), inf_(i) { ps().push_back(this); }
  void doStuff() { std::cout << id_ << " (" << inf_ << ")" << std::endl; }
  int  const        id_;
  char const *const inf_;
};

template <class T>
struct PWrap_T { static P p_s; };

// *** Can I guarantee this is done before main()? ***
template <class T>
P PWrap_T<T>::p_s(T::id(), T::desc());

#define PP(ID, DESC, NAME) /* semicolon must follow! */  \
struct ppdef_##NAME  {                                   \
  constexpr static int         id()   { return ID; }     \
  constexpr static char const *desc() { return DESC; }   \
};                                                       \
PWrap_T<ppdef_##NAME> const NAME

// In a compilation unit apart from the template/macro header.
void dump() {
  std::cout << "[";
  for (P *pp : ps()) { std::cout << " " << pp->id_ << ":" << pp->inf_; }
  std::cout << " ]" << std::endl;
}

// In some compilation unit.
void bar(int cnt) {
  for (int i = 0; i < cnt; ++i) {
    PP(2, "description", pp);
    pp.p_s.doStuff();
  }
}

int main() {
  dump();
  PP(3, "another", pp2);
  bar(5);
  pp2.p_s.doStuff();
}

(C++11 §3.6.2/4 - [basic.start.init]:)

It is implementation-defined whether the dynamic initialization of a non-local variable with static storage duration is done before the first statement of main. If the initialization is deferred to some point in time after the first statement of main, it shall occur before the first odr-use (3.2) of any function or variable defined in the same translation unit as the variable to be initialized.

... A non-local variable with static storage duration having initialization with side-effects must be initialized even if it is not odr-used (3.2, 3.7.1).

Also, trying __attribute__ ((init_priority(int))) or __attribute__ ((constructor)) for the template member's initialization yielded warning: attributes after parenthesized initializer ignored, and I know no other tricks regarding static initialization.

Thanks in advance to anyone who can give me an answer about this!

Jeff
  • 3,475
  • 4
  • 26
  • 35
  • I imagine the `odr-use` rule is meant to cover dynamic shared objects (DSOs) that might have file-scope objects. You obviously cannot initialize everything in a DSO if it's brought in after main starts by `dlopen()`, but in theory `dlopen()` can ensure everything in the DSO is initialized before you call anything else in the DSO. I imagine the answer ultimately is defined by the ABI for whatever OS / architecture you're compiling for. – Joe Z Dec 18 '13 at 07:27
  • The singleton pattern solves the issue, doesn't it? – lkanab Dec 24 '13 at 08:02

2 Answers2

3

The standard guarantees that static storage duration objects are initialized before any functions/variables in the same translation unit as your object are used from an external source.

The wording here is designed to work with shared libraries. Because shared libraries can be dynamically loaded after main() has started the language specification has to be flexible enough to cope with it. But as long as you access your object from outside the translation unit then you are guaranteed that it will have been constructed before you are given accesses (unless you are doing something pathological).

BUT this does not stop it being used before initialization if it is used in the constructor of another static storage duration object in the same compilation unit.

But you can easily manually provide guarantees that a static object is correctly initialized before used by using the technique below.

Just change the static variable to a static function. Then inside the function declare a static member that is returned. So you can use it exactly the same way as before (just add ()).

template <class T>
struct PWrap_T
{
    static P& p_s();  // change static variable to static member function.
                      // Rather than the type being P make it a reference to P
                      // because the object will be held internally to the function
};

template <class T>
P& PWrap_T<T>::p_s()
{
    // Notice the member is static.
    // This means it will live longer than the function.
    // Also it will be initialized on first use.
    // and destructed when other static storage duration objects are destroyed.
    static P p_s_item(T::id(), T::desc());

    return p_s_item;

    // Note its not guaranteed to be created before main().
    // But it is guaranteed to be created before first use.
}

So here you get the benefits of a global (whatever they are). You get guaranteed construction/destruction and you know the object will be correctly constructed before it can be used.

The only change you need to make is:

void bar(int cnt) {
  for (int i = 0; i < cnt; ++i) {
    PP(2, "description", pp);
    pp.p_s().doStuff();
     //   ^^  Add the braces here.
  }
}
Martin York
  • 257,169
  • 86
  • 333
  • 562
  • Only problem with this is that function-statics aren't threadsafe (the initialization of them, at least) in pre-C++11 iirc. – Bwmat Dec 21 '13 at 07:01
  • @Bwmat: True the language does not guarantee it pre C++11. But gcc does (even pre C++11). – Martin York Dec 21 '13 at 07:03
  • @Loki You are correct that this is the guaranteed safe approach, but my concern is with getting the static initializations done both safely and early if possible. The Meyers singleton approach you use doesn't allow me to initialize and enumerate all my `P` instances before `main()`, which is my real goal. The objects have a very heavy construction cost, and it would be useful to generate full lists of instances at the very start of run time (as well as avoiding runtime locking the list to make additions). – Jeff Dec 22 '13 at 21:12
  • @Jeff: As pointed out in the first two paragraphs. You should be OK with your method. The only issues is when you try and accesses static storage duration objects from the constructor/destructor of other static storage duration objects. Which is when you would use the technique above. – Martin York Dec 22 '13 at 21:21
1

As you've already found out the C++ standard doesn't guarantee that "the dynamic initialization of a non-local variable with static storage duration is done before the first statement of main". However, GCC does peform such initialization before executing main as described in How Initialization Functions Are Handled.

The only problem is initialization of static objects from shared libraries loaded with dlopen. These will only be initialized at the time the library is loaded, but there's nothing you can do about it.

vitaut
  • 49,672
  • 25
  • 199
  • 336