1

I'm wondering whether I can rely on constant initialization when there is a dependency between two constant non-local float variables with static storage duration in two different translation units - where one is dependent on (initialized to [the value of]) the other, and where for the latter, constant initialization is performed. I'm looking for answers providing and interpreting the relevant part of the standard, particularly the C++11 standard.

// Note: the non-use of constexpr is intended (C++03 compatibility)

// foo.h
struct Foo {
  static const float kValue;
};

// foo.cpp
const float Foo::kValue = 1.5F;

// bar.h
struct Bar {
  static const float kValue;
};

// bar.cpp
#include "foo.h"
const float Bar::kValue = Foo::kValue;  // Constant initialization?

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

int main() { std::cout << Bar::kValue; }
  • Is the non-local (constant) variable Bar::kValue, which have static storage duration, initialized by means of constant initialization? (Which in turn answers whether it is initialized by means of static or dynamic initialization)

Details / investigations of my own

[basic.start.init]/1 states [emphasis mine]:

Constant initializationis performed:

  • if each full-expression (including implicit conversions) that appears in the initializer of a reference with static or thread storage duration is a constant expression (5.19) and the reference is bound to an lvalue designating an object with static storage duration or to a temporary (see 12.2);

  • if an object with static or thread storage duration is initialized by a constructor call, and if the initialization full-expression is a constant initializer for the object;

  • if an object with static or thread storage duration is not initialized by a constructor call and if either the object is value-initialized or every full-expression that appears in its initializer is a constant expression.

In interpret from the final bullet that Bar::kValue is initialized by means of constant initialization if Foo::kValue is a constant expression. I suspect I can find the answer to whether this is true or not in [expr.const], but here I'm stuck.

dfrib
  • 70,367
  • 12
  • 127
  • 192
  • No, `Foo::kValue` is not a constant expression, because the compiler can't see its initializer in another TU and also because it is neither integral, enumeration, constexpr, nor temporary: https://timsong-cpp.github.io/cppwp/n3337/expr.const#2.9 – Oktalist May 21 '19 at 13:47
  • @Oktalist : `Foo::kValue` is declared as const, then it is a constant expression. Where to get the value is another problem – P. PICARD May 21 '19 at 13:56
  • @P.PICARD No, const and constant expression are two different things. – Oktalist May 21 '19 at 14:00
  • @Oktalist thanks. Would you like make that into an answer? (Alternatively prompt SergeBallesta to add the reference and reasoning to his answer) – dfrib May 21 '19 at 14:35

2 Answers2

1

Hmm... I would not trust that code because I would be afraid of a static initialization order fiasco. AFAIK, the order of static initialization between different compilation unit is undeterminated. That means that even tests would not make me confident that everything will always be ok.

Without entering into gory details, I could not find anything in standard that will ensure that Foo::kValue in foo.cpp will be initialized before Bar::kValue in bar.cpp. And if the order is wrong, the value in Foo::kValue will just be undeterminated.

Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • I know it is not a good answer because it gives no reference. I just posted it to warn OP of a potential problems. If I am wrong, just comment here explaining why and I will remove it. Or of course post another better answer. I promise to upvote it if you ping me and if I can understand it :-) – Serge Ballesta May 21 '19 at 13:37
  • Certainly nothing in C++11 standard that guarantees the order of non-trivial static initialisation between modules. You can rely on trivial and constexpr initialisation to be done at compile time. That's why singletons, though they have their own lifetime issues. I am surprised this is not addressed in the newer specs, but I don't think it has been, beyond constexpr. – Gem Taylor May 21 '19 at 14:30
  • Thanks, you are right. Oktalist provided a reference that he can either add to his own answer or expand to your answer. – dfrib May 21 '19 at 14:37
  • My opinion is that the compiler should do the job. I deleted my post supporting that hypothesis because indeed there is no statement in the spec supporting that idea. But there is a simple test to do: `Bar::kValue` and `Foo::kValue` depending on each other alternatively. MSVC does the job correctly, the executable prints 1.5 for both cases. But it is possible that it depends on the implementation and that other compilers behave differently. – P. PICARD May 21 '19 at 14:51
  • @P.PICARD I'm mostly interested in what the standard guarantees here; I wouldn't use this construct in product code as I'm unsure of what guarantees I get. Based on Oktalist's comments `Bar::kValue` is not initialized by means of constant initialization, and we may (however unlikely), from the standard's point of view, run into the static initialization fiasco. – dfrib May 21 '19 at 15:03
  • @SergeBallesta Do you want to update this answer incorporating the standard reference from Oktalist to his comment to the question? Basically they key is that `Foo::kValue` is not a _constant expression_, which means that `Bar::kValue` will be initialized by means of dynamic initialization. In my particular example, however, this is well-defined as `Foo::kValue` itself is initialized by means of constant initialization, but if I were to add, say, another static const float `Foo::kValueTwo` to `Foo` and initialize `Foo::kValue` to the value of `Foo::kValueTwo`, then both ... – dfrib May 22 '19 at 07:53
  • ... `Foo::kValue` and `Bar::kValue` would be initialized by means of dynamic initialization, meaning we could run into the static initialization fiasco as the order of dynamic initialization between different TU's is indeterminate. If you do not plan to update your answer I will summarize these points in an answer of my own. – dfrib May 22 '19 at 07:54
1

const float does not meet the requirements for being a constant expression

(This answer is based on @Oktalist's comment, as he refrained of making into an answer of his own)

In the following:

// foo.h
struct Foo {
  static const float kValue;
};

// foo.cpp
const float Foo::kValue = 1.5F;

Foo::kValue is indeed initialized by a constant expression by means of constant initialization, but Foo::kValue is not a constant expression itself, because it is neither integral, enumeration, constexpr, nor temporary. [expr.const]/2 states [emphasis mine]:

A conditional-expression is a core constant expression unless it involves one of the following as a potentially evaluated subexpression ([basic.def.odr]), but subexpressions of logical AND ([expr.log.and]), logical OR ([expr.log.or]), and conditional ([expr.cond]) operations that are not evaluated are not considered [ Note: An overloaded operator invokes a function. — end note ]:

...

(2.9): an lvalue-to-rvalue conversion ([conv.lval]) unless it is applied to

  • a glvalue of integral or enumeration type that refers to a non-volatile const object with a preceding initialization, initialized with a constant expression, or
  • a glvalue of literal type that refers to a non-volatile object defined with constexpr, or that refers to a sub-object of such an object, or
  • a glvalue of literal type that refers to a non-volatile temporary object whose lifetime has not ended, initialized with a constant expression;

As none of the sub-clauses for (2.9) applies here, Foo::kValue is not a constant expression. It follows from [basic.start.init]/2 (as quoted in an earlier standard version in the question) that Bar::kValue is not initialized by means of constant initialization, but as part of dynamic initialization.

Variables with static storage duration ([basic.stc.static]) or thread storage duration ([basic.stc.thread]) shall be zero-initialized ([dcl.init]) before any other initialization takes place [emphasis mine]:

Constant initialization is performed:

  • ...
  • if an object with static or thread storage duration is not initialized by a constructor call and if every full-expression that appears in its initializer is a constant expression.

A note on the "static initialization order fiasco"

Note that this particular example does not lead to the risk of the static initialization order fiasco, as Foo::kValue is initialized as means of constant initialization and Bar::kValue is initialized as part of dynamic initialization, and the former is guaranteed to complete before the start of dynamic initialization.

If the former would also be initialized as part of dynamic initialization, the initialization of the two would be indeterminately sequenced with respect to each other (and all other dynamic initialization).

Never rely on the fact, however, that this particular example has a well-defined initialization order, as subtle changes would invalidate this fact:

// foo.h
struct Foo {
  static const float kDummyValue;
  static const float kValue;
};

// foo.cpp
const float Foo::kDummyValue = 1.5F;    // Constant initialization
const float Foo::kValue = kDummyValue;  // (!) Dynamic initialization

// bar.h
struct Bar {
  static const float kValue;
};

// bar.cpp
#include "foo.h"
const float Bar::kValue = Foo::kValue;  // (!) Dynamic initialization

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

int main() { std::cout << Bar::kValue; }

As in this modifier example, the initialization of Foo::kValue and Bar::kValue is indeterminately sequenced with respect to each other, meaning Bar::kValue could be initialized (with the "value" of Foo::kValue) before Foo::kValue is.

dfrib
  • 70,367
  • 12
  • 127
  • 192