MSVC和constexpr对函数参数有影响吗?

9
这段代码可以在clang和gcc下编译通过。
template<size_t n>
struct N {
    static constexpr size_t v = n;
};

template<size_t n>
constexpr bool operator<(N<n>, size_t n2) {
    return n < n2;
}

template<typename N>
constexpr void foo(N v) {
    static_assert(v < 5);
}

int main()
{
    foo(N<3>{});
    return 0;
}

然而,如果我使用 MSVC 编译器,我会遇到错误,提示 v < 5 不是一个常量表达式。我可以理解为什么 MSVC 这样认为,但我认为这是错误的,clang / gcc 是正确的。这是 MSVC 的一个 bug 吗?


https://dev59.com/z1UL5IYBdhLWcg3wDkZT - RbMm
2
请纠正我,如果我错了,但是在这里this从未被显式或隐式地评估。 - Cássio Renan
@CássioRenan - 这里的常见问题不在于 this,而是因为读取了超出其生命周期的变量。这里应该使用 v 而不是 this - RbMm
如果你使用 const N v 会怎样? - Robert Andrzejuk
3个回答

5

是的,MSVC在这里是错误的。

代码看起来是符合要求的似乎有些反直觉,因为不是常量表达式的v怎么可能用于常量表达式中呢?

那么为什么允许呢?首先,考虑到非正式地说,如果表达式求值为一个既不是常量表达式也不是在封闭表达式之外开始生命的变量的glvalue,则该表达式不是常量表达式[expr.const]p2.7

其次,operator<constexpr的。

现在,发生的情况是v < 5是一个有效的常量表达式。为了理解这一点,让我们来看看表达式的求值过程。

我们有:

  1. v < 5调用您的constexpr operator<
  2. 两个参数都被复制(它们都是字面量,没有一个计算为非constexpr对象)
  3. n2v < 5的评估中开始其生命,并且是一个字面量
  4. n是一个非类型模板参数,因此可在常量表达式中使用
  5. 最后,n < n2调用内置运算符。

所有这些都不违反[expr.const]p2中的任何点,因此生成的表达式实际上是一个常量表达式,可以用作static_assert的参数。

这些类型的表达式称为转换后的常量表达式

以下是一个简化的示例:

struct Foo {
  constexpr operator bool() { return true; }
};

int main() {
  Foo f;
  static_assert(f);
}

1

MSVC在这里是错误的,让我们从简化版本的代码开始:

struct N {
    static constexpr size_t v = 0;
};

constexpr 
  bool operator<(N n1, size_t n2) {
    return n1.v < n2;
}

  void foo(N v) {
    static_assert(v < 5, ""); // C++11 does not allow terse form
}

我们首先看一下 static_assert,我们是否违反了任何常量表达式的规则?如果我们查看 [expr.const]p2.2

对于字面类或constexpr函数,调用非constexpr构造函数以外的函数 [注:通常应用重载解析(13.3)];

我们没问题,operator< 是 constexpr 函数,而 N 的复制构造函数是字面类的 constexpr 构造函数。
接下来看一下 operator< 并检查比较 n1.v < n2,如果我们查看 [expr.const]p2.9
除非应用于以下情况之一,否则会执行左值到右值的转换(4.1):
- 整数或枚举类型的glvalue,该glvalue引用先前初始化的非易失性const对象,并使用常量表达式初始化,或
- 文字类型的glvalue,该glvalue引用使用constexpr定义的非易失性对象,或者引用此类对象的子对象,或
- 指向尚未结束生存期的使用常量表达式初始化的非易失性临时对象的文字类型的glvalue。

在原始示例中,我们还涉及可在常量表达式中使用的模板非类型参数,因此相同的推理也适用于该案例。 < 的两个操作数都是可用的常量表达式。

我们还可以看到 MSVC仍将简化后的情况视为不合法, 尽管clang和gcc接受它。


提供一个constexpr复制构造函数,一切都能正常工作。这就是魔法 https://godbolt.org/z/hqefvo :') - Antoine Morrier

-1
如果你已经声明了:
template<size_t n>
struct N {
  int i;
  static constexpr size_t v = n;
 };

演示 这里

MSVC、Clang 和 GCC 都会拒绝你的代码。原因是 v 被复制到 operator< 的第一个参数中。这样的复制是对 v 的求值,而 v 不是常量表达式。

在你的情况下,N 是一个空类。我不认为标准规定这样的类的复制构造函数是否应该访问对象的内存1核心语言问题 1701)。因此,编译器显示的行为取决于是否访问了空类对象的内存。

当作为参数传递时,Clang 和 GCC 不会访问空类对象的内存,但 MSVC 会:请参见此 编译器资源管理器链接

所以我认为所有编译器都是正确的。


1 访问对象的内存表示以复制填充位将涉及到 reinterpret_cast(或等效), 这在常量表达式中也是被禁止的。


2
为了检查表达式的常量性,标准只关心是否发生了左值到右值的转换。它并不关心内存是否被访问。 - xskxzr
@xskxzr 嘿嘿,当应用lvalue-to-rvalue时,即使标准没有明确说明它发生在这里,也会发生:核心问题2103 - Oliv
@xskxzr 我添加了一条注释,解释了除了左值到右值转换之外可能发生的事情! - Oliv
@xskxzr 我理解这不是标准制定者的本意。标准是一种形式主义,它是不完整和不一致的(就像任何复杂和有趣的形式主义一样),但它仍然比无法表达的意图更加一致! - Oliv
CWG1701与复制构造函数访问内存有什么关系?复制构造函数是根据它所复制的成员/基类来定义的。没有基类或成员就没有复制。 - Rakete1111
显示剩余5条评论

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