为什么这个表达式不是常量表达式?

19

该代码中的表达式 b 必须是一个核心常量表达式

int main()
{
    constexpr int a = 10;
    const int &b = a;
    constexpr int c = b; // Here
    return 0;
}

根据标准 (参见n4700,8.20第2段[expr.const]):

除非表达式 e 的求值结果是下列表达式之一,否则表达式 e 就是一个“核心常量表达式”(core constant expression):

  • ...

  • 一个 lvalue-to-rvalue 转换(7.1),除非它应用于:

    • ...

    • 一个指向定义为 constexpr 的非易变对象 或该对象的非可变子对象的非易变 glvalue,或者

首先,在上述代码中,表达式 b 是一个左值(也是一个 glvalue)因为它是一个引用,从而成为一个变量(参见8.1.4.1第1段[expr.prim.id.unqual]):

如果实体是函数、变量或数据成员,则该表达式是左值;否则它是 prvalue。如果标识符指定了位域,则它是位域(11.5)。

其次,变量 b 所表示的对象是 a,且该对象使用 constexpr 声明。但 GCC 报错:

./hello.cpp: In function ‘int main()’:
./hello.cpp:6:20: error: the value of ‘b’ is not usable in a constant expression
  constexpr int c = b;
                    ^
./hello.cpp:5:13: note: ‘b’ was not declared ‘constexpr’
  const int &b = a;
据我所知,引用不是一个对象,因此上述项目显然建议使用constexpr声明a。我是否遗漏了什么? 我不同意GCC的原因是GCC将b视为对象,因此需要使用constexpr声明它。但是,b不是一个对象!

2
Clang 赞同 GCC。但我认为我同意你的观点,这是标准或编译器中的缺陷。值得注意的是,MSVC 接受该代码。 - Sebastian Redl
2个回答

18
核心常量表达式的规则之一是,我们无法评估以下内容:引用类型的变量或数据成员的标识表达式,除非该引用具有前导初始化,并且
- 它使用常量表达式进行了初始化; 或者 - 它的生存期始于 e 的计算中; b 是一个带有前导初始化的引用类型变量的标识表达式。然而,它是从 a 初始化的。那么,a 是一个常量表达式吗?根据[expr.const]/6
- 如果一个 glvalue 核心常量表达式引用的实体是符合以下定义的常量表达式的允许结果,则该表达式是一个常量表达式:[...] - 如果它是一个具有静态存储期的对象,而不是临时对象或其值满足上述约束条件的临时对象,或者是一个函数,则该实体是常量表达式的允许结果。 a 是一个 glvalue 核心常量表达式(它不符合 expr.const/2 中的任何限制),但它既不是具有静态存储期的对象,也不是函数。
因此,a 不是常量表达式。因此,b 也不是从一个常量表达式进行初始化的,因此不能在核心常量表达式中使用。因此,c 的初始化是非法的,因为它不是常量表达式。将 a 声明为 static constexpr int,GCC 和 Clang 都会接受该程序。
C++,你神奇的野兽。

3
b 被一个 glvalue 初始化,而 a 作为一个 glvalue 不是常量表达式。 - cpplearner
请注意,clang拒绝编译"constexpr int const& b = a;",并显示"reference to 'a' is not a constant expression"。 - Massimiliano Janes
1
[expr.const]/6: "常量表达式是指一个glvalue核心常量表达式,它引用的实体是常量表达式的允许结果(如下所定义),或者是一个prvalue核心常量表达式[...]。如果一个实体是具有静态存储期的对象,并且不是临时对象,或者是一个值满足上述约束条件的临时对象,或者是一个函数,则该实体是常量表达式的允许结果。" - cpplearner
@SebastianRedl,是的,同样的推理似乎允许得出结论,即b没有用常量表达式初始化...因此中和了Barry的引言? - Massimiliano Janes
3
@AnT,我不同意。Bit-field要求一个“整数常量表达式”,这不是“常量表达式”的子集,而是“整数类型的表达式,隐式转换为prvalue,其中转换后的表达式是核心常量表达式。”这是不同的要求。 - Barry
显示剩余6条评论

-1

本答案中提供的所有引用均来自当前工作草案。


根据你的例子:

int main()
{
    constexpr int a = 10;
    const int &b = a;
    constexpr int c = b; // here
    return 0;
}

首先,让我们逐行分析你的例子。第一行是一个定义常量表达式变量的语句,变量名为标识符a
constexpr int a = 10;

初始化器10是一个整数常量表达式,因此它是一个核心常量表达式,根据[expr.const]/9:

整数常量表达式是一个具有整数或未作用域枚举类型的表达式,隐式转换为prvalue,其中转换的表达式是核心常量表达式。[注意:这些表达式可以用作位域长度,如果基础类型不固定,则用作枚举器初始化程序,并用作对齐方式。 - end note]

它是一个常量表达式,因为它是满足[expr.const]/12约束的prvalue核心常量表达式:
常量表达式是一个prvalue核心常量表达式,其值满足以下约束条件:
  • (12.1)如果该值是类类型的对象,则每个非静态数据成员引用的引用类型指向的实体是常量表达式的允许结果;
  • (12.2)如果该值是指针类型,则它包含具有静态存储持续时间的对象的地址,这样一个对象的结尾之后的地址([expr.add])、非立即函数的地址或空指针值;
  • (12.3)如果该值为指向成员函数的指针类型,则不指定立即函数;
  • (12.4)如果该值是类或数组类型的对象,则每个子对象都符合该值的这些约束条件。
因此,初始化表达式`10`是一个既不是指针类型也不是类类型或数组类型的prvalue核心常量表达式。因此,它是一个常量表达式。
第二行声明/定义了一个名为标识符`b`的引用变量,它引用了标识符`a`。
 const int &b = a;

为了知道初始化表达式a是否是核心常量表达式,您必须调用[expr.const]/5

除非按照抽象机器的规则([intro.execution])评估E将评估以下之一,否则表达式E是核心常量表达式:[..]

此时,E的评估不会评估[expr.const]/5中定义的任何约束。因此,表达式E是核心常量表达式。
但是,E是一个glvalue核心常量表达式,并不意味着E也是常量表达式;因此,您必须检查[expr.const]/12
一个常量表达式可以是一个glvalue核心常量表达式,它引用的实体是常量表达式的允许结果之一[...]如果一个实体是具有静态存储期的对象,它既不是临时对象,也是一个满足上述约束条件的临时对象的值,或者是一个非立即函数,则该实体是常量表达式的允许结果之一。
因此,实体a不是常量表达式的允许结果,因为该实体既不引用具有静态存储期的对象,也不是临时对象或非立即函数。因此,glvalue核心常量表达式a不是常量表达式。
第三行定义了一个名为标识符c的constexpr变量,由变量b初始化:
constexpr int c = b;

你需要再次调用[expr.const]/5以了解表达式b是否是核心常量表达式。正如你已经注意到的,因为初始化c涉及lvalue-to-rvalue转换,所以可能会满足(5.8)这个条件。根据[expr.const]/5规定:

表达式E是核心常量表达式,除非评估E[...]会评估出以下之一:

  • (5.8)一个lvalue-to-rvalue转换,除非它应用于
    • (5.8.1) 一个指向在常量表达式中可用的对象的非易失glvalue;或者
    • (5.8.2)一个指向在E的评估内开始生存期的非易失文字类型的对象的非易失glvalue;
请注意,我所提到的文档中没有以下项目符号。但是C++20文档有它。我不知道为什么它被从当前工作草案中删除了。([expr.const]/(5.12)):
(5.12)一个id表达式,它引用引用类型的变量或数据成员,除非该引用具有前置初始化并且满足以下条件之一:
(5.12.1)它可用于常量表达式;
(5.12.2)其生命周期始于E的评估内部;
即使检查过了,表达式E仍然不是核心常量表达式,因为该引用在常量表达式中不可用(见下文),并且其生命周期不会在E的评估内开始。
回到我们的解释。由于表达式E计算了lvalue-to-rvalue转换,因此满足项目符号(5.8)。因此首先尝试(5.8.1)。

实际上,左值到右值的转换应用于非易失性 glvalue(b)。但是,这个 glvalue 是否指的是在常量表达式中可用的对象?首先,您必须检查它是否是一个常量初始化变量,然后还必须检查它是否是一个潜在常量变量,因此首先检查[expr.const]/2

如果变量或临时对象o被满足以下条件,则其为常量初始化

  • (2.1) 它具有初始化程序或其默认初始化导致执行某些初始化,并且
  • (2.2) 其初始化的完整表达式在解释为常量表达式时是常量表达式[..]

第一个子弹点(2.1)已经满足,因为变量b具有初始化程序。但是第二个子弹点不满足,因为其初始化的完整表达式不是常量表达式,因为其初始化程序a不是常量表达式,如前所述。因此,变量b不是常量初始化

仅为完整起见,[expr.const]/3也被尝试:

如果一个变量是constexpr或具有引用或const限定的整数或枚举类型,则它可能是常量。

由于变量b具有引用类型,因此它被认为是可能是常量的。请注意,无需检查变量b是否为潜在常量,因为从直觉上讲,您甚至不能进入[expr.const]/4,因为它要求变量是常量初始化的:

常量初始化的可能常量变量可在某一点P处的常量表达式中使用,如果其初始化声明D可以从P到达[..]

(强调我自己)

尽管变量可能是常量,但由于它没有被常量初始化,因此无法在常量表达式中使用。因此,我们得出结论,变量b不能在常量表达式中使用(正如GCC诊断提示的那样)。因此,子条款(5.8.1)未满足:因此,表达式E不是核心常量表达式,因此也不是常量表达式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接