EDG与 GCC/Clang在常量表达式中对右值引用行为的差异
我写了以下程序,它被MSVC与 EDG接受,但被clang与 gcc拒绝。 演示。
template<int N>
struct s{
constexpr static const int&& n = N;
constexpr static int&& p = N;
};
template<typename T> void f()
{
static_assert(T::n == 1);
}
template<typename T> void g()
{
static_assert(T::p == 1);
}
int main(){
f<s<1>>(); //#1: works in all 4 compilers
g<s<1>>(); //#2: works in edg & msvc but not in clang and gcc
}
可见,#2 被edg和 msvc接受,但被clang和 gcc拒绝。我的想法是 #2 应该无效,因为它是可修改的(例如我们可以写 s<1>::p = 3;,在所有编译器下都能编译通过)。但我想知道根据最新的C++标准,#2 的标准行为是什么。
解决方案
TL;DR
重要的不是引用对象的属性,而是它所引用的对象的属性及其生命周期。
它所引用的是从 N 物化的一个临时对象。
T::n 绑定一个不可变的 int const,其生命周期是 T::n 的生命周期:可以在常量表达式中使用。
T::p 绑定一个可变的 int:不能在常量表达式中使用。
因此GNU编译器是对的,EDG和 MSVC错误。
细节
在 static_assert(T::<whatever> == 1) T::<whatever> 的情形中,需进行左值到右值转换(lvalue-to-rvalue conversion):
https://eel.is/c++draft/conv.lval#1%5C
https://eel.is/c++draft/expr.arith.conv
特别是 https://eel.is/c++draft/expr.arith.conv#1.1:
左值到右值转换([conv.lval])应用于每个操作数,所得的右值用于本节其余部分的替换原操作数。
那么从 T::<whatever> 的左值到右值转换得到的右值,是否可在常量表达式中使用?
https://eel.is/c++draft/expr.const#core-2
一个表达式E 是核心常量表达式,除非按照抽象机器的规则求值E 的过程会导致下列情况之一:
…
(2.9) 左值到右值的转换,除非它应用于
(2.9.1) 类型为cv std::nullptr_t的 glvalue,
(2.9.2) 指向在常量表达式中可用的对象的非易变glvalue,或
(2.9.3) 指向一个在E 的求值过程中开始生命周期的非易变对象的字面量类型的非易变glvalue;
…
(无关的要点略)
对 T::<whatever> 的左值到右值转换得到的是一个指向从 N 物化的临时对象的glvalue。
对于 T::n,其类型为 int const,对于 T::p,其类型为 int。
这个临时对象在常量表达式中可用吗?
https://eel.is/c++draft/expr.const#init-9
一个对象或引用在点P 时可在常量表达式中使用,当它在点P 时可能在常量表达式中使用,并且在点P 可constexpr表示。
constexpr-representable在这里定义:https://eel.is/c++draft/expr.const#init-3,但在这里已经得到满足。
那么这个临时对象是否在潜在意义上可以在常量表达式中使用?
https://eel.is/c++draft/expr.const.init#8
一个对象或引用在点P 时可能在常量表达式中使用,若它满足下列条件:
(8.1) 它是由一个在点P 时可在常量表达式中使用的变量所声明的对象,
(8.2) 是一个常量限定的字面量类型的临时对象,其生命周期被扩展到可在常量表达式中使用的变量的生命周期;
(8.3) 是一个模板参数对象,
(8.4) 是一个字符串字面量对象,
(8.5) 上述对象的一个不可变子对象,或
(8.6) 上述对象的任意引用成员。
8.1:它不是引用
8.2:对于 T::n 是,因为它是一个 int const,其生命周期被扩展到与 T::n 相同的生命周期,但对于 T::p 不是,因为它是一个非-const int。
8.3:它不是模板参数
8.4:它不是字符串字面量对象
8.5:它不是上述对象的一个不可变子对象
8.6:它不是上述对象的任意引用成员
因此通过对 T::p 进行左值到右值转换得到的右值不能在常量表达式中使用,gcc/clang拒绝该代码的判断是正确的。
此外还应注意,constexpr 实际上并非复现此问题所必需的:
template<int N>
struct s{
static const int&& n;
static int&& p;
};
template<int N> const int&& s<N>::n = N;
template<int N> int&& s<N>::p = N;
LIVE
它显示了相同的行为。
甚至我们还可以将示例简化为:
template<int N> const int&& nv = N;
template<int N> int&& pv = N;
static_assert(nv<1> == 1);
static_assert(pv<1> == 1);
然而在这里EDG正确地拒绝了这段代码。