Article

Taming the Static-Initialization Order Fiasco

April 10, 2020

Three translation units, one silent failure.

// service.h
#include <string>

struct Service {
  static const std::string NAME;
};

// service.cpp
#include "service.h"
const std::string Service::NAME = "SERVICE1";

// main.cpp
#include "service.h"
#include <iostream>

const std::string MSG = "service " + Service::NAME + " ready";

int main() {
  std::cout << MSG << std::endl;
}

Build succeeds, output isservice  ready.Service::NAME was read before it was constructed: classic static-initialization order fiasco (SIOF).

Lazy but Safe

struct Service {
  static const std::string& NAME() {
    static const std::string _name = "SERVICE1";
    return _name;
  }
};

const std::string& MSG() {
  static const std::string _msg =
    "service " + Service::NAME() + " ready";
  return _msg;
}

int main() {
  std::cout << MSG() << std::endl;
}
  • Guaranteed correct order
  • First-call latency and locking
  • Resources released after main finishes

Zero-Overhead Variant

Build the whole message at compile-time with a tiny header-only helper.

// static_string.h (C++11)
template <int... I> struct seq {};
template <int N, int... I> struct gen : gen<N-1,N-1,I...> {};
template <int... I> struct gen<0,I...> { typedef seq<I...> type; };

template <int N, typename Tag> struct sstr;
struct ref_tag {}; struct arr_tag {};

template <int N>
struct sstr<N,ref_tag> {
  const char (&p)[N+1];
  constexpr char operator[](int i) const { return p[i]; }
};

template <int N>
struct sstr<N,arr_tag> {
  char p[N+1];
  template <int N1,int... A,int... B>
    constexpr sstr(const sstr<N1,ref_tag>& a,
                   const sstr<N-N1,ref_tag>& b,
                   seq<A...>,seq<B...>)
      : p{a[A]...,b[B]...,0} {}
  constexpr char operator[](int i) const { return p[i]; }
  constexpr const char* c_str() const { return p; }
};

template <int N> constexpr auto lit(const char (&a)[N])
{ return sstr<N-1,ref_tag>{a}; }

template <int N1,int N2>
constexpr auto operator+(const sstr<N1,ref_tag>& a,
                         const sstr<N2,ref_tag>& b)
{ return sstr<N1+N2,arr_tag>{a,b,
    typename gen<N1>::type{},typename gen<N2>::type{}}; }

Use it as a drop-in compile-time string builder.

#include "static_string.h"
#include <iostream>

constexpr auto NAME = lit("SERVICE1");
constexpr auto MSG  = lit("service ") + NAME + lit(" ready");

int main() {
  std::cout << MSG.c_str() << std::endl;   // fully static, no SIOF
}

C++17 Quality-of-Life

template <int N>
sstr(const char (&)[N]) -> sstr<N-1,ref_tag>;          // CTAD
constexpr auto NAME = sstr{"SERVICE1"};                // size deduced
constexpr auto MSG  = sstr{"service "} + NAME + sstr{" ready"};

Checklist

  • Avoid globals that depend on other globals.
  • If you must: wrap in a function-local static.
  • Prefer constexpr objects; the compiler enforces safety.
  • For strings, a tiny static_string library yields zero-cost correctness even in C++11.