【问题标题】:Constant initialization of dependent non-local constant float variables w/ static storage duration in different translation units不同翻译单元中具有静态存储持续时间的依赖非局部常量浮点变量的常量初始化
【发布时间】:2019-10-07 20:43:12
【问题描述】:

我想知道当在两个不同的翻译单元中具有静态存储持续时间的两个常量非局部浮点变量之间存在依赖关系时,我是否可以依赖常量初始化 - 其中一个依赖于(初始化为 [the value of] ) 另一个,对于后者,执行常量初始化。我正在寻找提供和解释标准相关部分的答案,尤其是 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 声明 [emphasis 我的]:

执行不断的初始化:

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

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

  • 如果具有静态或线程存储持续时间的对象不是 由构造函数调用初始化,如果任一对象是 值初始化或出现在其 初始化器是一个常量表达式

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

【问题讨论】:

  • 不,Foo::kValue 不是常量表达式,因为编译器在另一个 TU 中看不到它的初始化器,也因为它既不是整数、枚举、constexpr 也不是临时的:timsong-cpp.github.io/cppwp/n3337/expr.const#2.9
  • @Oktalist : Foo::kValue 被声明为 const,那么它就是一个常量表达式。从哪里获取价值是另一个问题
  • @P.PICARD 不,const 和常量表达式是两个不同的东西。
  • @Oktalist 谢谢。你想把它变成答案吗? (或者提示 SergeBallesta 在他的答案中添加参考和推理)

标签: c++ c++11 language-lawyer static-initialization


【解决方案1】:

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

在没有深入细节的情况下,我在标准中找不到任何可以确保foo.cpp 中的Foo::kValue 将在bar.cpp 中的Bar::kValue 之前初始化。如果顺序错误,Foo::kValue 中的值将只是不确定的。

【讨论】:

  • 我知道这不是一个好的答案,因为它没有提供参考。我刚刚发布它是为了警告 OP 存在潜在问题。如果我错了,请在这里评论解释原因,我会删除它。或者当然发布另一个更好的答案。如果您 p​​ing 我并且如果我能理解它,我保证会投票赞成 :-)
  • 当然,C++11 标准中没有任何东西可以保证模块之间非平凡的静态初始化的顺序。您可以依靠在编译时完成的琐碎和 constexpr 初始化。这就是为什么单身人士,尽管他们有自己的终生问题。我很惊讶新的规范中没有解决这个问题,但我认为除了 constexpr 之外还没有解决。
  • 谢谢,你是对的。 Oktalist 提供了一个参考,他可以将其添加到自己的答案中,也可以扩展到您的答案。
  • 我认为编译器应该完成这项工作。我删除了支持该假设的帖子,因为规范中确实没有声明支持该想法。但是有一个简单的测试要做:Bar::kValueFoo::kValue 交替依赖于彼此。 MSVC 正确地完成了这项工作,两种情况下的可执行文件都打印 1.5。但它可能取决于实现并且其他编译器的行为不同。
  • @P.PICARD 我最感兴趣的是标准在这里保证了什么;我不会在产品代码中使用这个结构,因为我不确定我能得到什么保证。基于 Oktalist 的 cmets Bar::kValue 不是通过常量初始化来初始化的,从标准的角度来看,我们可能(尽管不太可能)遇到静态初始化惨败。
【解决方案2】:

const float 不满足作为常量表达式的要求

(此答案基于@Oktalist's comment,因为他没有做出自己的答案)

如下:

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

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

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

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

...

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

  • 整数或枚举类型的左值,它引用具有先前初始化的非易失性 const 对象,已初始化 使用常量表达式,或
  • 字面量类型的左值,指的是用 constexpr 定义的非易失性对象,或者指的是这样的子对象 对象,或
  • 一个字面量类型的左值,它指向一个生命周期尚未结束的非易失性临时对象,用一个常量初始化 表达;

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

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

不断初始化

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

关于“static initialization order fiasco”的注释

请注意,此特定示例不会导致静态初始化顺序失败的风险,因为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 的“值”)是。

【讨论】:

    猜你喜欢
    • 2020-02-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-06
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    相关资源
    最近更新 更多