2

I have a static global variable initialized with a function call. This causes the initialization order to be not respected, eve inside a translation unit. This is a reproducer (the order of the code makes little sense here but it resembles the original arrangement in my use case, and is important to trigger the issue, so please disregard it):

#include <array>
#include <iostream>
#include <limits>

template <typename T> constexpr T defaultValue = std::numeric_limits<T>::max();

namespace _Private {
template <typename T> std::array<T, 3> getDefaultArrayValue() {
  std::cout << "Initialize array" << std::endl;
  std::array<T, 3> result;
  result[0] = defaultValue<T>;
  result[1] = defaultValue<T>;
  result[2] = defaultValue<T>;
  return result;
}
} // namespace _Private

// This generates the wrong behavior
template <typename T> const std::array<T, 3> defaultValue<std::array<T, 3>> = _Private::getDefaultArrayValue<T>();
// This behaves right
//template <typename T> const std::array<T, 3> defaultValue<std::array<T, 3>> = {defaultValue<T>, defaultValue<T>, defaultValue<T>};

struct TestClass {
  std::array<float, 3> arr;
  TestClass();
};

TestClass tc;

int main() {

  std::cout << tc.arr[0] << std::endl;

  return 0;
}

TestClass::TestClass() : arr{defaultValue<std::array<float, 3>>} {std::cout << "Build TestClass" << std::endl;}

Here I have a TestClass class whose constructor initializes the arr member variable to its default value, as defined by the defaultValue<std::array<float,3>> specialization; the latter is a global variable whose value is set equal to the return value of getDefaultArrayValue(). A global instance of TestClass is created, and I would expect that its arr member is correctly initialized to the specified default, that is 3.40282e+38 (i.e. the maximum float value). However, running the program the value 0. is printed, because the initialization of defaultValue<std::array<float, 3>> is actually done after the creation of tc:

$ ./a.out 
Build TestClass
Initialize array
0

so when tc is built defaultValue<std::array<float, 3>> still does not have its correct value. The problem is fixed if defaultValue<std::array<float, 3>> is initialized by directly assigning a value to it (see the commented line in the reproducer code) rather than calling a function, restoring the "from first to last" static variable initialization order rule that applies to a single translation unit.

To summarize, it seems that calling a function to initialize a global variable breaks the initialization order. Is this behavior mandated by the standard or is it an implementation detail (I'm using GCC 11.2.0)?

Nicola Mori
  • 777
  • 1
  • 5
  • 19
  • 2
    This is implementation detail. The classic solution is to have a function with a `static` variable return a reference to it. – Sam Varshavchik Apr 29 '22 at 14:14
  • Read also this: https://en.cppreference.com/w/cpp/language/siof and this: https://stackoverflow.com/questions/1005685/c-static-initialization-order – Jabberwocky Apr 29 '22 at 14:44
  • @Jabberwocky I am aware of the SIOF, but that applies only to variables defined in different translation units. And in my case there is a single TU so the initialization order should be "from top to bottom" as stated at the end of the cppreference page you linked. – Nicola Mori Apr 29 '22 at 15:30
  • @SamVarshavchik I tried your proposal but it does not work. Making `getDefaultArrayValue` return a reference to a static variable, and `defaultValue>` a reference itself, I get a segfault. I guess this is due to `getDefaultArrayValue` still not being executed timely and thus `defaultValue>` being a dangling reference. – Nicola Mori Apr 29 '22 at 15:44
  • 1
    I don't know what "tried" means, I can only comment on actual code that I see. And in the code that I do see, `defaultValue>` is definitely not a reference, so I don't know what "a reference itself" means, in relation to that. – Sam Varshavchik Apr 29 '22 at 15:48
  • Sorry, I meant that I changed the definition to `template const std::array &defaultValue> = _Private::getDefaultArrayValue();`, and that I modified the function to `template std::array &getDefaultArrayValue() { . . . `, and defined the `result` variable as `static std::array result;`. – Nicola Mori Apr 29 '22 at 15:59
  • I don't believe I said anything about changing anything to a reference. I only wrote "return a reference". Big difference. Also "function with a `static` variable" means something very specific in C++ too. Basically, try only what I wrote "have a function with a `static` variable return a reference to it" exactly and precisely, nothing more, nothing less, translated literally and directly into C++. – Sam Varshavchik Apr 29 '22 at 16:09
  • If I understand you suggest to change `defaultValue` from a variable template to a function template returning a reference to a static variable? I believe that this would work, but I cannot make `defaultValue` a function template, it will break lots of stuff around. – Nicola Mori Apr 29 '22 at 16:53

1 Answers1

0

It’s not that you used a function call: it’s that that function isn’t constexpr, and that it’s a (specialization of a) variable template being initialized. The former prevents constant initialization, and the latter removes all ordering constraints for dynamic initialization. Obviously in this case the fix can be trivial.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • Thanks for sharing your insight. Actually I cannot make the function a `constexpr`, since according to g++ " 'struct std::array' has no user-provided default constructor and the implicitly-defined constructor does not initialize 'float std::array::_M_elems [3]' ". I guess I'll have to circumvent this error in some way. – Nicola Mori Apr 30 '22 at 09:09
  • @NicolaMori: Initialize it with `{}` and then assign? – Davis Herring Apr 30 '22 at 12:41
  • That works, but needs C++17 while my project uses C++14. Anyway, I guess this is the best solution. Time to move to C++17... – Nicola Mori May 01 '22 at 07:49
  • @NicolaMori: You could initialize it directly rather than via assignment if that matters, but yes you should upgrade if you can! – Davis Herring May 01 '22 at 18:38
  • Yes, in this case I could, but the usage of `std::vector` is just for the simplicity of this reproducer. Actually the container is a sort of std::array but with a non-ordered index type. Since there's no natural map between the index value and a position, the container has no constructor taking an initialization list. So it has to be default built and then each element must be set using `operator[]` as in the reproducer. – Nicola Mori May 02 '22 at 06:04