首页 > 解决方案 > 为什么动态初始化发生在 gcc 中的静态初始化之前

问题描述

#include <iostream>
struct NonConstant{
    NonConstant(int v):v_(v){
        std::cout<<"NonConstant\n";
    }
    int v_;
};

struct Constant{
    constexpr Constant(int v):v_(v){
        if(v_==0){
         std::cout<<"Constant\n";
        }
    }
    int v_;
};

NonConstant a = 2; //#1
Constant b = 0;   //#2

int main(){
}

结果是:

NonConstant
Constant

我对这个结果感到困惑,因为根据标准规则,#1不是静态初始化,#2是因为这些:

变量或临时对象 o 的常量初始值设定项是其完整表达式为常量表达式的初始值设定项,除非 o 是对象,这样的初始值设定项还可以为 o 及其子对象调用constexpr 构造函数,即使这些对象不属于-literal 类类型。
如果具有静态或线程存储持续时间的变量或临时对象由实体的常量初始化程序初始化,则执行常量初始化。如果不执行常量初始化,则将具有静态存储持续时间或线程存储持续时间的变量初始化为零。零初始化和常量初始化一起称为静态初始化;所有其他初始化都是动态初始化。所有静态初始化都强烈发生在 ([intro.races]) 任何动态初始化之前

class 的构造函数NonConstant不是由 constexpr 指定的, 的初始化NonConstant a = 2;会调用 object 的非 constexpr 的构造函数a,因此 for 的初始化#1不是静态初始化,而是动态初始化。相比之下, 的初始化Constant b = 0;是静态初始化,因为被调用的构造函数是 constexpr 构造函数。并且规则说所有静态初始化强烈发生在任何动态初始化之前。那么,为什么结果意味着 的评估#1发生在 的评估之前#2?如果我错过了什么,请纠正我。

更新:

在这个问题的下面的评论中,有人说除了构造函数的类可以是非字面量类型外,constexpr构造函数在任何方面都必须是有效的核心常量表达式,即调用std::cout会使constexpr构造函数不核心常量表达式。但是,我在cppreference中找到了另一种解释,即:

常量初始化在(C++14 之前)而不是(C++14 起)静态和线程局部对象的零初始化之后以及所有其他初始化之前执行。只有以下变量是常量初始化的:

  1. [...]
  2. 由构造函数调用初始化的类类型的静态或线程局部对象,如果构造函数是constexpr 并且所有构造函数参数(包括隐式转换)都是常量表达式,并且如果构造函数的初始化程序列表中的初始化程序和大括号-或-类成员的相等初始化器只包含常量表达式

它并不是说 constexpr 构造函数必须是核心常量表达式。只要调用的构造函数满足它的限定条件constexpr并且它的参数都必须是常量表达式,并且成员初始化器必须是常量表达式。所以,#2确实是一个常量初始化,因为参数0是一个常量表达式,并且被指定符限定的选定构造函数constexpr和成员初始化器遵守expr.const中提到的这些规则。

标签: c++c++17language-lawyer

解决方案


b有动态初始化,没有静态初始化。

正如您对[basic.start.static]/2b的引用已经解释的那样,仅当其初始化程序的完整表达式(即Constant(int)构造函数的执行)是常量表达式时,才具有静态初始化。

[expr.const]/2中,我们读到:

一个表达式e是一个核心常量表达式,除非根据e抽象机的规则,对 的求值将求值以下表达式之一:

  • ...

  • 对文字类的 constexpr 构造函数以外的函数的调用、constexpr 函数或对普通析构函数 ([class.dtor]) 的隐   式调用;

  • ...

这里对“遵循抽象机规则”的构造函数的求值包括构造函数体。并且由于初始化程序是0,该评估将调用std::operator<<(std::ostream&, const char*),而不是constexpr。所以初始化器的完整表达式不是核心常量表达式,也不是常量表达式。

当然,虽然这不是严格的技术定义,但“常量表达式”的全部意义在于定义我们何时保证编译器可以在编译时处理某些事情。并且写入程序的标准输出肯定不会在编译时发生。

cppreference.com 是一个试图尽可能准确的好资源,但它不能替代实际标准的权威性。关于使用类构造函数进行常量初始化的引用对于 C++14 和 C++17 是不正确的。我怀疑它实际上是从 C++11 遗留下来的,其中constexpr构造函数的主体根本不允许评估任何函数调用,并且 [expr.const] 类似地描述了constexpr在核心常量表达式中使用构造函数的要求的成员初始化程序。


推荐阅读