为什么我下面的第二个片段显示未定义的行为?
Why is it that my second snippet below shows undefined behavior?
clang
和 g++
似乎都符合 C++ 标准中段落 [expr.const]/5 的最新版本。以下代码段为两个编译器打印 11。见 live example:
#include <iostream>
void f(void) {
static int n = 11;
static int* temp = &n;
static constexpr int *&&r = std::move(temp);
std::cout << *r << '\n';
}
int main()
{
f();
}
根据我对这一段的理解,两个编译器都应该为下面的代码打印 2016
。但他们没有。因此,我必须得出结论,该代码显示了未定义的行为,因为 clang
打印了一个任意数字,而 g++
打印了 0
。我想知道为什么是 UB,例如,考虑到标准的 N4527 草案? Live example.
#include <iostream>
void f(void) {
static int n = 11;
static int m = 2016;
static int* temp = &n + 1;
static constexpr int *&&r = std::move(temp);
std::cout << *r << '\n';
}
int main()
{
f();
}
编辑
我习惯于不满足于只说代码是 UB 或显示未定义行为的答案。我总是喜欢更深入地研究,有时,就像现在一样,我碰巧足够幸运地多了解一点编译器是如何构建的。这就是我在这种情况下发现的:
对于任何大于 -O0
的优化级别,clang
和 GCC
似乎都从代码中消除了任何未使用的变量,如 m
。 GCC
似乎以静态存储持续时间对局部变量进行排序,与变量放置在堆栈上的方式相同,即从高地址到低地址。
因此,在 clang
中,如果我们将优化级别更改为 -O0
,我们将按预期打印数字 2016
。
在GCC
中,如果除此之外,我们还更改了
的定义
static int* temp = &n + 1;
至
static int* temp = &n - 1;
我们也会得到代码打印的数字2016
。
我认为这里没有任何微妙之处。 &n + 1
指向 array-of-one 的 one-past-the-end,您可以认为位置 n
,因此它不构成可取消引用的指针,尽管它是一个完全有效的指针。因此 temp
和 r
是非常好的 constexpr 变量。
您可以这样使用 r
:
for (int * p = &n; p != r; ++p) { /* ... */ }
这个循环甚至可以出现在 constexpr 函数中。
当您尝试取消引用 r
时,行为当然是未定义的,但这与常量表达式无关。
您显然期望您可以:
- 获取指向静态存储持续时间对象的指针
- 加一
- 获取指向 "next" 静态存储持续时间对象的指针(按声明顺序)
这是胡说八道。
您必须避开 所有 standard-backed 保证,仅依赖 UB 和实现文档的邪恶组合。很明显,在我们讨论 constexpr
和 std::move
之前很久你就已经超过了 UB 门槛,所以我不确定他们在这个问题中的意义是什么。
clang
和 g++
似乎都符合 C++ 标准中段落 [expr.const]/5 的最新版本。以下代码段为两个编译器打印 11。见 live example:
#include <iostream>
void f(void) {
static int n = 11;
static int* temp = &n;
static constexpr int *&&r = std::move(temp);
std::cout << *r << '\n';
}
int main()
{
f();
}
根据我对这一段的理解,两个编译器都应该为下面的代码打印 2016
。但他们没有。因此,我必须得出结论,该代码显示了未定义的行为,因为 clang
打印了一个任意数字,而 g++
打印了 0
。我想知道为什么是 UB,例如,考虑到标准的 N4527 草案? Live example.
#include <iostream>
void f(void) {
static int n = 11;
static int m = 2016;
static int* temp = &n + 1;
static constexpr int *&&r = std::move(temp);
std::cout << *r << '\n';
}
int main()
{
f();
}
编辑
我习惯于不满足于只说代码是 UB 或显示未定义行为的答案。我总是喜欢更深入地研究,有时,就像现在一样,我碰巧足够幸运地多了解一点编译器是如何构建的。这就是我在这种情况下发现的:
对于任何大于 -O0
的优化级别,clang
和 GCC
似乎都从代码中消除了任何未使用的变量,如 m
。 GCC
似乎以静态存储持续时间对局部变量进行排序,与变量放置在堆栈上的方式相同,即从高地址到低地址。
因此,在 clang
中,如果我们将优化级别更改为 -O0
,我们将按预期打印数字 2016
。
在GCC
中,如果除此之外,我们还更改了
static int* temp = &n + 1;
至
static int* temp = &n - 1;
我们也会得到代码打印的数字2016
。
我认为这里没有任何微妙之处。 &n + 1
指向 array-of-one 的 one-past-the-end,您可以认为位置 n
,因此它不构成可取消引用的指针,尽管它是一个完全有效的指针。因此 temp
和 r
是非常好的 constexpr 变量。
您可以这样使用 r
:
for (int * p = &n; p != r; ++p) { /* ... */ }
这个循环甚至可以出现在 constexpr 函数中。
当您尝试取消引用 r
时,行为当然是未定义的,但这与常量表达式无关。
您显然期望您可以:
- 获取指向静态存储持续时间对象的指针
- 加一
- 获取指向 "next" 静态存储持续时间对象的指针(按声明顺序)
这是胡说八道。
您必须避开 所有 standard-backed 保证,仅依赖 UB 和实现文档的邪恶组合。很明显,在我们讨论 constexpr
和 std::move
之前很久你就已经超过了 UB 门槛,所以我不确定他们在这个问题中的意义是什么。