有时候局部类无法访问在函数作用域中定义的constexpr变量。

huangapple go评论65阅读模式
英文:

Why sometimes local class cannot access constexpr variables defined in function scope

问题

这段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;
}

但是这个代码可以工作:

#include <iostream>
#include <vector>

int main()
{
    constexpr int kInt = 123;
    struct LocalClass {
        void func(){
            const int b = std::max((int)kInt, 12); // 添加了额外的转换 "(int)"
            std::cout << b;
            const int c = kInt; // 这也可以
            std::cout << c;
            const auto d = std::vector{kInt}; // 也可以工作
            std::cout << d[0];
        }
    };
    LocalClass a;
    a.func();
    return 0;
}

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

英文:

This c++ code cannot compile:

#include &lt;iostream&gt;

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 &lt;&lt; b;
        }
    };
    LocalClass a;
    a.func();
    return 0;
}

But this works:

#include &lt;iostream&gt;
#include &lt;vector&gt;

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

Tested under C++17 and C++20, same behaviour.

答案1

得分: 5

  1. 在一般情况下,本地实体不能从嵌套函数定义中的作用域中进行odr使用(就像您的LocalClass示例中一样)。

这由以下规则给出:
> 6.3 单一定义规则 [basic.def.odr]
> <sup>(10)</sup> 如果一个本地实体在作用域中是odr可用的,则:
> [...]
> <sup>(10.2)</sup> 对于在实体引入点和作用域之间的每个介入作用域(其中*this被认为在最内层封闭类或非lambda函数定义作用域中引入),要么:
> - 介入作用域是块作用域,或者
> - 介入作用域是具有命名该实体的简单捕获或具有捕获默认值lambda表达式的函数参数作用域,并且lambda表达式的块作用域也是介入作用域。

>
> 如果一个本地实体在其不可odr使用的作用域中odr使用,程序就是非法的。

因此,您只能在嵌套块作用域和捕获本地变量的lambda中odr使用本地变量。

即:

void foobar() {
    int x = 0;

    {
        // OK:x在这里是odr可用的,因为只有介入的块作用域
        std::cout &lt;&lt; x &lt;&lt; std::endl;
    }

    // OK:x在这里是odr可用的,因为它被lambda捕获
    auto l = [&amp;]() { std::cout &lt;&lt; x &lt;&lt; std::endl; };

    // NOT OK:存在介入的函数定义作用域
    struct K {
      int bar() { return x; }
    };
}

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

因此,如果使用kInt构成odr使用,您的程序将自动非法。

  1. 一般来说,命名一个变量构成对该变量的odr使用:
    > 6.3 单一定义规则 [basic.def.odr]
    > <sup>(5)</sup> 如果表达式是表示它的id-expression,那么变量通过表达式命名。如果变量x由可能被评估的表达式E命名,那么通过E,x被odr使用,除非[...]

但是因为kInt是一个常量表达式,特殊例外情况(5.2)可能适用:
> 6.3 单一定义规则 [basic.def.odr]
> <sup>(5.2)</sup> x是一个可用于常量表达式的非引用类型变量,没有可变子对象,并且E是非易失性限定的非类类型表达式的潜在结果集的元素,应用了左值到右值转换,或

因此,命名kInt只要满足以下条件就不被视为odr使用...

  • 是非引用类型(✓)
  • 可以在常量表达式中使用(✓)
  • 不包含可变成员(✓)

并且包含kInt的表达式...

  • 必须产生非易失性限定的非类类型(✓)
  • 必须应用左值到右值转换(?)

因此,几乎所有检查都适用于不将kInt命名为odr使用,因此是良好定义的。

在您的示例中唯一不始终满足的条件是必须进行左值到右值转换。

如果不进行左值到右值转换(即不引入临时变量),则您的程序是非法的 - 如果进行左值到右值转换,则它是合法的。

// 将会应用左值到右值转换到kInt:
// (合法)
const int c = kInt;  
std::vector v{kInt}; // 向量构造函数接受std::size_t

// 不会应用左值到右值转换到kInt:
// (它被传递给std::max的引用)
// (非法)
std::max(kInt, 12); // std::max接受const引用的参数(!)

这也是为什么std::max((int)kInt, 12);是合法的原因 - 显式转换引入了临时变量,因为应用了左值到右值转换。

英文:

1. odr-using local entities from nested function scopes

Note that kInt still has automatic storage duration - so it is a local entity as per:
> 6.1 Preamble [basic.pre]
> <sup>(7)</sup> A local entity is a variable with automatic storage duration, [...]


In general local entities cannot be odr-used from nested function definitions (as in your LocalClass example)

This is given by:
> 6.3 One-definition rule [basic.def.odr]
> <sup>(10)</sup> A local entity is odr-usable in a scope if:
> [...]
> <sup>(10.2)</sup> for each intervening scope between the point at which the entity is introduced and the scope (where *this is considered to be introduced within the innermost enclosing class or non-lambda function definition scope), either:
> - the intervening scope is a block scope, or
> - the intervening scope is the function parameter scope of a lambda-expression that has a simple-capture naming the entity or has a capture-default, and the block scope of the lambda-expression is also an intervening scope.
>
> If a local entity is odr-used in a scope in which it is not odr-usable, the program is ill-formed.

So the only times you can odr-use a local variable within a nested scope are nested block scopes and lambdas which capture the local variable.

i.e.:

void foobar() {
    int x = 0;

    {
        // OK: x is odr-usable here because there is only an intervening block scope
        std::cout &lt;&lt; x &lt;&lt; std::endl;
    }

    // OK: x is odr-usable here because it is captured by the lambda
    auto l = [&amp;]() { std::cout &lt;&lt; x &lt;&lt; std::endl; };

    // NOT OK: There is an intervening function definition scope
    struct K {
      int bar() { return x; }
    };
}

11.6 Local class declarations [class.local] contains a few examples of what is and is not allowed, if you're interested.


So if use of kInt constitutes an odr-use, your program is automatically ill-formed.

2. Is naming kInt always an odr-use?

In general naming a variable constitutes an odr-use of that variable:
> 6.3 One-definition rule [basic.def.odr]
> <sup>(5)</sup> A variable is named by an expression if the expression is an id-expression that denotes it. A variable x that is named by a potentially-evaluated expression E is odr-used by E unless [...]

But because kInt is a constant expression the special exception (5.2) could apply:
> 6.3 One-definition rule [basic.def.odr]
> <sup>(5.2)</sup> x is a variable of non-reference type that is usable in constant expressions and has no mutable subobjects, and E is an element of the set of potential results of an expression of non-volatile-qualified non-class type to which the lvalue-to-rvalue conversion is applied, or

So naming kInt is not deemed an odr-use as long as it ...

  • is of non-reference type (✓)
  • is usable in constant expressions (✓)
  • does not contain mutable members (✓)

and the expression that contains kInt ...

  • must produce a non-volatile-qualified non-class type (✓)
  • must apply the lvalue-to-rvalue conversion (?)

So we pass almost all the checks for the naming of kInt to not be an odr-use, and therefore be well-formed.

The only condition that is not always true in your example is the lvalue-to-rvalue conversion that must happen.

If the lvalue-to-rvalue conversion does not happen (i.e. no temporary is introduced), then your program is ill-formed - if it does happen then it is well-formed.

// lvalue-to-rvalue conversion will be applied to kInt:
// (well-formed)
const int c = kInt;  
std::vector v{kInt}; // vector constructor takes a std::size_t

// lvalue-to-rvalue conversion will NOT be applied to kInt:
// (it is passed by reference to std::max)
// (ill-formed)
std::max(kInt, 12); // std::max takes arguments by const reference (!)

This is also the reason why std::max((int)kInt, 12); is well-formed - the explicit cast introduces a temporary variable due to the lvalue-to-rvalue conversion being applied.

答案2

得分: 2

std::max 接受它的参数通过 const 引用(这里是 int const&amp;),并返回通过该引用传递的值。

因此,在 const int b = std::max(kInt, 12); 中,kInt 是对自动对象 main()::kInt 的引用;main()::LocalClass::func() 无法访问 main() 的堆栈帧,因此无法形成该引用。这是幸运的,否则 kInt 可能会成为悬空引用(如果在 main() 返回后调用 LocalClass::func())。例如,此函数存在悬空引用错误:

auto f() {
    constexpr int kInt = 123;
    return [&](int i) { return std::max(i, kInt); };
    //                                     ^^^^ 悬空引用 f()::kInt
}

kInt 转换为 int 执行 lvalue-to-rvalue 转换,在这种情况下绕过了访问 kInt 的存储,因为编译器知道它是 constexpr,不能取除 123 以外的任何值。

英文:

Language-lawyer answer: std::max(kInt, 12) odr-uses kInt, since std::max accepts a constant reference which must be initialized by [dcl.init.ref]/1, by [dcl.init.ref]/5.1. However, std::max((int)kInt, 12) does not odr-use kInt by [basic.def.odr]/5.2. main()::LocalClass cannot odr-use kInt by [class.local]/1.


std::max takes its parameters by const reference (here int const&amp;), and returns that reference passed through.

So, in const int b = std::max(kInt, 12);, kInt is a reference to the automatic object main()::kInt; main()::LocalClass::func() has no way to access the stack frame of main() so it is unable to form that reference. This is fortunate, since otherwise kInt could be a dangling reference (if you were to call LocalClass::func() after main() returns). For example, this function has a dangling reference bug:

auto f() {
    constexpr int kInt = 123;
    return [&amp;](int i) { return std::max(i, kInt); };
    //                                     ^^^^ dangling reference to f()::kInt
}

Casting kInt to int performs lvalue-to-rvalue conversion, which in this case bypasses accessing the storage of kInt since the compiler knows that it is constexpr and cannot take any other value than 123.

huangapple
  • 本文由 发表于 2023年6月2日 00:15:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76383859.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定