在不同翻译单元中具有静态存储持续时间的依赖非局部常量浮点变量的常量初始化

Constant initialization of dependent non-local constant float variables w/ static storage duration in different translation units

本文关键字:常量 依赖 持续时间 非局 初始化 浮点变量 存储 静态 翻译 单元      更新时间:2023-10-16

我想知道当两个具有静态存储持续时间的常量非局部浮点变量之间存在依赖关系时,我是否可以依赖常量初始化 - 其中一个依赖于(初始化为 [值])另一个,而对于后者,执行常量初始化。我正在寻找提供和解释标准相关部分的答案,特别是 C++11 标准。

// 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; }
  • 具有静态存储持续时间的非局部(常量)变量Bar::kValue是否通过常量初始化进行初始化?(这反过来又回答它是通过静态还是动态初始化的方式初始化)

我自己的细节/调查

[basic.start.init]/1 声明 [强调我的]:

常量初始化执行:

  • 如果每个完整表达式(包括隐式转换)都 出现在带有静态或线程的引用的初始值设定项中 存储持续时间是一个常量表达式 (5.19),引用为 绑定到指定具有静态存储持续时间的对象的 lvalue 或临时(见12.2);

  • 如果初始化了具有静态或线程存储持续时间的对象 通过构造函数调用,并且如果初始化完整表达式为 对象的常量初始值设定项;

  • 如果具有静态或线程存储持续时间的对象不是 由构造函数调用初始化,如果对象是 值初始化或在其中显示的每个完整表达式 初始值设定项是一个常量表达式

在解释从最后一个项目符号中,如果Bar::kValue是常量表达式,则Foo::kValue通过常量初始化进行初始化。我怀疑我可以在 [expr.const] 中找到这是否属实的答案,但在这里我被困住了。

嗯...我不会信任该代码,因为我害怕静态初始化顺序惨败。AFAIK,不同编译单元之间的静态初始化顺序是不确定的。这意味着即使是测试也不会让我相信一切都会好起来的。

在没有进入血腥细节的情况下,我无法在标准中找到任何可以确保在bar.cppBar::kValue之前初始化foo.cppFoo::kValue的内容。如果顺序错误,则Foo::kValue的值将无法确定。

const float不符合

常量表达式的要求

(这个答案是基于@Oktalist的评论,因为他没有做出自己的回答)

在以下方面:

// foo.h
struct Foo {
static const float kValue;
};
// foo.cpp
const float Foo::kValue = 1.5F;

Foo::kValue确实是通过常量初始化常量表达式初始化的,但Foo::kValue本身不是常量表达式,因为它既不是积分、枚举、constexpr 也不是临时的。[expr.const]/2 声明 [强调我的]:

一个条件表达式是一个核心常量表达式,除非它涉及以下作为潜在计算的子表达式之一([basic.def.odr]), 但逻辑 AND 的子表达式 ([expr.log.and]), 逻辑或 ([expr.log.or]), 和有条件的 ([expr.cond]) 未计算的操作不被视为 [ 注意:一个 重载运算符调用函数。— 尾注 ]:

(2.9):左值到右值的转换([conv.lval])除非它适用于

  • 整数或枚举类型的 GL值,引用具有前面初始化的非易失性 const 对象,初始化 使用常量表达式,或
  • 文字类型的 glvalue 引用使用 constexpr 定义的非易失性对象,或引用此类对象的子对象 对象,或
  • 文本类型的 gl值,它引用生存期尚未结束的非易失性临时对象,使用常量初始化 表达;

由于(2.9)的任何子句都不适用于此处,因此Foo::kValue不是一个常量表达式。从 [basic.start.init]/2(如问题中较早的标准版本中引用)可以看出,Bar::kValue不是通过常量初始化进行初始化,而是作为动态初始化的一部分。

具有静态存储持续时间([basic.stc.static])或线程存储持续时间([basic.stc.thread])的变量应在任何其他初始化发生之前进行零初始化([dcl.init]):

执行常量初始化

  • 如果具有静态或线程存储持续时间的对象未由构造函数调用初始化,并且如果每个完整表达式 出现在其初始值设定项中的是一个常量表达式

关于"静态初始化订单惨败"的说明

请注意,此特定示例不会导致静态初始化顺序惨败的风险,因为Foo::kValue初始化为常量初始化,Bar::kValue初始化为动态初始化的一部分,并且前者保证在动态初始化开始之前完成。

如果前者也将作为动态初始化的一部分进行初始化,则两者的初始化将相对于彼此(以及所有其他动态初始化)进行不确定的排序。

但是,永远不要依赖这个特定示例具有明确定义的初始化顺序这一事实,因为细微的更改会使这一事实无效:

// 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; }

就像在这个修饰符示例中一样,Foo::kValueBar::kValue的初始化是相对于彼此不确定的排序的,这意味着Bar::kValue可以在Foo::kValue之前初始化(使用Foo::kValue的"值")。