【问题标题】:static constexpr in function scope - is MSVC's behavior a compiler bug?函数范围内的静态 constexpr - MSVC 的行为是编译器错误吗?
【发布时间】:2017-10-25 05:18:23
【问题描述】:

我的理解是函数作用域static constexpr 在编译时被评估。如果是这种情况,MSVC 对以下错误有什么理由(如果有):

int main()
{
    int i = 5;

    switch (i)
    {
        // Original question:
        static constexpr int j = 7; // legal?
        // My actual use case, which may not be legal.
        static constexpr int k[2] = { 7, 4 };

    default:
    case 0:
        break;
    }
    return 0;
}

testapp.cpp(10) : 错误 C2360 : 'j' 的初始化被 'case' 标签跳过

如果那是非 constexpr,那么是的,这是一个有效的投诉。然而,由于 constexpr 是在编译时评估的,因此不需要在声明站点执行任何操作。

-- 编辑--

向 Martin Bonner 道歉,他的回答因不适用于我原来的问题而被删除。

我的实际用例是第二个:带有初始化列表的 constexpr 数组。从我在引用的标准中看到的情况来看,我的第一个静态 constexpr scalar int 案例是不被禁止的。但是,看来我正在尝试做的事情是不正确的。

如果这确实是真的,那为什么? constexpr 的全部意义不是在编译时评估事物,因此,控制是否真正通过声明并不重要。

【问题讨论】:

  • fyi g++ 5.1.0 编译干净。
  • 我会说这是一种非常奇怪的方式来编写 switch 语句。在它之外引入另一个范围来放置您的变量。 {static constexpr int j = 7; swtich(i){...}}
  • constexpr 表示它在编译时是已知的,但除此之外,它的语义与任何其他变量相同(加上 const-ness)
  • @AndyG 当然,我可以在开关上方立即声明它,这是我目前所做的。但这是一个保持声明接近其使用的问题。正是出于这个原因,C++ 让我们能够在块中声明变量。我在 switch 中只在几个情况下使用了数组,所以我想尽可能地保持它的声明接近它的用途。
  • @DavidG:是的,这很公平。这是一个非常好的问题,让我研究了很多关于初始化和 switch 语句的规则,所以感谢你有机会发表答案。

标签: c++ function scope static constexpr


【解决方案1】:

正如@PasserBy 在评论中暗示的那样,constexpr 不会像您希望的那样影响。在static constexpr int a = 10;之后,读取a的值的代码可以在编译时优化为直接使用10,编译器会保证初始化器10是一个编译时常量,但是还是有一个反对那里在某些情况下可能需要在运行时实际存储,包括初始化。例如,当它的地址被占用时。

块作用域statics 的初始化可能会提前发生,与文件作用域对象的方式相同,或者它可能会在执行到达声明时发生:

N4140 [dcl.stmt]p4:

[...] 在允许实现静态初始化的相同条件下,允许实现对具有静态或线程存储持续时间的其他块范围变量进行早期初始化 在命名空间范围内具有静态或线程存储持续时间的变量 (3.6.2)。否则,此类变量在控件第一次通过其声明时被初始化;这样的变量在其初始化完成时被认为已初始化。 [...]

现在,@MartinBonner 的已删除答案引用了 [dcl.stmt]p3:

可以转移到块中,但不能通过初始化绕过声明。一个程序从具有自动存储持续时间的变量不在范围内的点跳转到 90 除非变量具有标量类型、具有普通默认构造函数和普通析构函数的类类型、这些类型之一的 cv 限定版本或 前面的类型,并且在没有初始化程序的情况下声明 (8.5)。

第二句仅针对具有自动存储持续时间的对象,但第一句没有。您正在以一种确实可能绕过初始化声明的方式转移到一个块中。唯一的一点是,它不会必要绕过带有初始化的声明:如果初始化提前执行,则不会绕过任何初始化。

在这种特殊情况下,我希望所有明智的实现都执行早期初始化,但这不是必需的,因此我认为错误消息是允许的。但我确实怀疑这不是 MSVC 的预期行为:

这是一个你肯定想要一个错误的例子:

int &f() { static int i; return i; }

int main() {
  switch (0) {
    static int &i = f();
  case 0:
    return i;
  }
}

这里,变量i 的初始化被跳过,结果访问了一个未初始化的引用,触发了多个编译器的分段错误。然而在这种情况下,MSVC 确实显示编译错误。 (其他编译器也没有。)在错误无害的情况下发出错误消息是没有意义的,而在错误是有害的情况下则省略。因此,我怀疑您收到的错误消息不是故意的,值得作为错误报告。

【讨论】:

  • 读了几次,在我看来我犯的错误(?)是假设因为使用constexpr保证编译时评估是可能的,它要求它必须发生。您引用的第一个段落明确指出,即使存在constexpr,运行时初始化也是合法的。 @dyp 对这个问题的回答中的第一个代码 sn-p:*.com/questions/26152096/… 显示了为什么 constconstexpr 在运行时基本相同的示例。
  • 我删除了我的答案,因为尽管第一句话是任何声明,但它似乎只指汽车。我认为其中有一个 DR。
【解决方案2】:

这是一个 MSVC 错误。

跳入变量声明+初始化器的范围是非法的(例如case 0: int j = 5;),但规则还说static在控件第一次通过其声明时被初始化。然而,根据 §6.4.2/6 [stmt.switch]

,允许声明存在于 switch 语句体中

"...声明可以出现在 开关语句。”

所以static 在这种情况下让一切正常。

如果你在 switch 语句的开头有一个非静态声明(没有初始化),你会完全没问题;是在初始化时跳过声明导致编译器出错。

switch (i)
{
    int j;
    case 0:
        j = 2;
        std::cout << 0 << std::endl;
    break;
    default:
        j = 3;
      std::cout << j << std::endl;
    break;
}

即使这样也很好(虽然很危险):

case 0:
    int j;
    j = 5;
break;
default:
    /*..*/

Because of the rules surrounding control transfer:§6.7/3 [stmt.dcl]:

可以转移到块中,但不能通过初始化绕过声明。

我认为cppreference 解释得更好:

如果控制转移进入任何自动变量的范围(例如,通过向前跳转声明语句),则程序是格式错误的(无法编译),除非进入范围的所有变量都有

1) 没有初始化器声明的标量类型

...

【讨论】:

  • 它看起来确实像一个 MSVC 错误(惊喜!),但你说的看起来不对。声明总是被允许的,编译器抱怨 initialization
  • @PasserBy:我同意你的看法。随着我进行更多调查,这对我来说变得越来越模棱两可。似乎关于静态初始化的规则可能与关于 switch 语句的规则相冲突,并且是否存在优先级令人困惑。它可能只是UB。因此,我将删除我的答案。 (高代表用户仍然可以看到它)
  • @PasserBy:我做了更多的研究并相应地更新了帖子。我觉得比以前更准确了。
最近更新 更多