英文:
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 <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;
}
But this works:
#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;
}
Tested under C++17 and C++20, same behaviour.
答案1
得分: 5
- 在一般情况下,本地实体不能从嵌套函数定义中的作用域中进行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 << x << std::endl;
}
// OK:x在这里是odr可用的,因为它被lambda捕获
auto l = [&]() { std::cout << x << std::endl; };
// NOT OK:存在介入的函数定义作用域
struct K {
int bar() { return x; }
};
}
11.6 本地类声明 [class.local] 包含了一些允许和不允许的示例,如果您感兴趣的话。
因此,如果使用kInt
构成odr使用,您的程序将自动非法。
- 一般来说,命名一个变量构成对该变量的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 << x << std::endl;
}
// OK: x is odr-usable here because it is captured by the lambda
auto l = [&]() { std::cout << x << 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&
),并返回通过该引用传递的值。
因此,在 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&
), 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 [&](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
.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论