为什么有时局部类无法访问函数作用域中定义的 constexpr 变量

flm*_*620 11 c++ language-lawyer c++17 c++20

此 C++ 代码无法编译:

#include <iostream>

int main()
{
    constexpr int kInt = 123;
    struct LocalClass {
        void func(){
            const int b = std::max(kInt, 12); 
            //                     ^~~~  
            // error: use of local variable with automatic storage from containing function
            std::cout << b;
        }
    };
    LocalClass a;
    a.func();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

但这有效:

#include <iostream>
#include <vector>

int main()
{
    constexpr int kInt = 123;
    struct LocalClass {
        void func(){
            const int b = std::max((int)kInt, 12); // added an extra conversion "(int)"
            std::cout << b;
            const int c = kInt; // this is also ok
            std::cout << c;
            const auto d = std::vector{kInt}; // also works
            std::cout << d[0];
        }
    };
    LocalClass a;
    a.func();
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在 C++17 和 C++20 下测试,行为相同。

Tur*_*ght 5

1. odr-使用嵌套函数作用域中的本地实体

\n

请注意,kInt仍然具有自动存储持续时间 - 因此它是一个本地实体,如下所示:

\n
\n

6.1 前言 [basic.pre]
\n (7)本地实体是具有自动存储期限的变量,[...]

\n
\n
\n

一般来说,本地实体不能从嵌套函数定义中使用 odr(如您的LocalClass示例中所示)

\n

这是由下式给出的:

\n
\n

6.3 单一定义规则 [basic.def.odr]
\n (10)如果满足以下条件,则本地实体在某个范围内可 odr 使用:
\n[...]
\n (10.2)对于以下点之间的每个介入范围:引入实体和范围(其中*this被认为是在最内层封闭类或非 lambda 函数定义范围内引入):

\n
    \n
  • 中间作用域是块作用域,或者
  • \n
  • 中间作用域是具有命名实体的simple-capture或具有capture-default 的lambda 表达式的函数参数作用域,并且lambda 表达式的块作用域也是中间作用域。
  • \n
\n

如果本地实体在不可 odr 可用的范围内进行 odr 使用,则该程序格式错误。

\n
\n

因此,唯一可以在嵌套作用域内使用局部变量的情况是嵌套块作用域和捕获局部变量的 lambda。

\n

IE:

\n
void foobar() {\n    int x = 0;\n\n    {\n        // OK: x is odr-usable here because there is only an intervening block scope\n        std::cout << x << std::endl;\n    }\n\n    // OK: x is odr-usable here because it is captured by the lambda\n    auto l = [&]() { std::cout << x << std::endl; };\n\n    // NOT OK: There is an intervening function definition scope\n    struct K {\n      int bar() { return x; }\n    };\n}\n
Run Code Online (Sandbox Code Playgroud)\n

11.6 本地类声明 [class.local]包含一些允许和不允许的示例,如果您感兴趣的话。

\n
\n

因此,如果使用kInt构成 ODR 使用,则您的程序自动是格式错误的。

\n

2. 命名kInt总是一种 ODR 用途吗?

\n

一般来说,命名一个变量就构成了该变量的 ODR 使用:

\n
\n

6.3 单一定义规则 [basic.def.odr]
\n (5)如果表达式是表示它的 id 表达式,则该变量由表达式命名。由潜在求值表达式 E 命名的变量 x 被 E odr 使用,除非 [...]

\n
\n

但由于kInt常量表达式,(5.2)因此可能适用特殊例外:

\n
\n

6.3 单定义规则 [basic.def.odr]
\n (5.2) x 是非引用类型的变量,可用于常量表达式,并且没有可变子对象,E 是 的潜在结果集合中的一个元素应用左值到右值转换的非易失性限定非类类型的表达式,或

\n
\n

因此,只要命名kInt不被视为 ODR 使用......

\n
    \n
  • 是非引用类型 (\xe2\x9c\x93)
  • \n
  • 可用于常量表达式 (\xe2\x9c\x93)
  • \n
  • 不包含可变成员 (\xe2\x9c\x93)
  • \n
\n

kInt以及包含...的表达式

\n
    \n
  • 必须生成非易失性限定的非类类型 (\xe2\x9c\x93)
  • \n
  • 必须应用左值到右值转换(?)
  • \n
\n

因此,我们通过了几乎所有的命名检查,kInt以确保其不被 ODR 使用,因此格式良好。

\n

在您的示例中,唯一不总是正确的条件是必须发生的左值到右值的转换。

\n

如果左值到右值的转换没有发生(即没有引入临时值),那么你的程序就是格式错误的——如果它确实发生了,那么它就是格式良好的。

\n
// lvalue-to-rvalue conversion will be applied to kInt:\n// (well-formed)\nconst int c = kInt;  \nstd::vector v{kInt}; // vector constructor takes a std::size_t\n\n// lvalue-to-rvalue conversion will NOT be applied to kInt:\n// (it is passed by reference to std::max)\n// (ill-formed)\nstd::max(kInt, 12); // std::max takes arguments by const reference (!)\n
Run Code Online (Sandbox Code Playgroud)\n

这也是格式良好的原因std::max((int)kInt, 12);- 由于应用了左值到右值的转换,显式转换引入了临时变量。

\n