我在一本书中读到了这一行:
事实证明,构建真正可以确定C ++函数是否会更改特定变量值的编译器是不可能的。
该段讨论的是为什么在检查常量性时编译器为什么会保守。
为什么不可能构建这样的编译器?
编译器始终可以检查是否重新分配了变量,是否在其上调用了非常量函数或是否将其作为非常量参数传入...
我在一本书中读到了这一行:
事实证明,构建真正可以确定C ++函数是否会更改特定变量值的编译器是不可能的。
该段讨论的是为什么在检查常量性时编译器为什么会保守。
为什么不可能构建这样的编译器?
编译器始终可以检查是否重新分配了变量,是否在其上调用了非常量函数或是否将其作为非常量参数传入...
Answers:
为什么不可能构建这样的编译器?
出于同样的原因,您不能编写将确定任何给定程序是否终止的程序。这被称为停止问题,这是不可计算的事情之一。
明确地说,您可以编写可以确定某个函数确实在某些情况下更改了变量的编译器,但是您不能编写能够可靠地告诉您该函数将或不会更改变量(或暂停)的编译器。每个可能的功能。
这是一个简单的例子:
void foo() {
if (bar() == 0) this->a = 1;
}
仅查看代码,编译器如何确定是否foo
会改变a
?是否执行取决于函数外部的条件,即的实现bar
。停止问题不是可计算的,这还不止于此,但在链接的Wikipedia文章(以及每本计算理论教科书)中已经很好地说明了暂停问题,因此在这里我不会尝试正确地解释它。
想象这样的编译器存在。我们还假设,为方便起见,它提供了一个库函数,如果所传递的函数修改了给定变量,则该函数返回1,否则,则返回0。那么该程序应打印什么?
int variable = 0;
void f() {
if (modifies_variable(f, variable)) {
/* do nothing */
} else {
/* modify variable */
variable = 1;
}
}
int main(int argc, char **argv) {
if (modifies_variable(f, variable)) {
printf("Modifies variable\n");
} else {
printf("Does not modify variable\n");
}
return 0;
}
f
修改变量,而不是是否可以修改变量。这个答案是正确的。
modifies_variable
编译器源复制/粘贴代码,从而完全使您的参数无效。(假设开源,但要点很明确)
不要混淆“在给定这些输入的情况下是否会修改变量”,因为“具有修改变量的执行路径”。
前者称为不透明谓词确定,并且几乎无法决定-除了减少停顿问题外,您还可以指出输入可能来自未知来源(例如,用户)。这是真正的所有语言,而不仅仅是C ++。
但是,可以通过查看分析树来确定后一条语句,这是所有优化编译器都可以执行的操作。它们这样做的原因是,纯函数 (以及对于透明参照的定义,是透明参照函数)具有各种可以应用的不错的优化,例如易于使用或在编译时确定其值。但是要知道一个函数是否是纯函数,我们需要知道它是否可以修改变量。
因此,关于C ++的令人惊讶的陈述实际上是关于所有语言的琐碎陈述。
我认为“ C ++函数是否会更改特定变量的值”中的关键词是“将”。当然可以构建一个编译器来检查是否允许 C ++函数更改特定变量的值,您不能确定地说更改即将发生:
void maybe(int& val) {
cout << "Should I change value? [Y/N] >";
string reply;
cin >> reply;
if (reply == "Y") {
val = 42;
}
}
const
-ness检查时所想到的。
可以做到这一点,并且编译器一直在为某些函数做这些事情,例如,这对于简单的内联访问器或许多纯函数来说是微不足道的优化。
在一般情况下是不可能知道的。
每当有来自另一个模块的系统调用或函数调用,或对潜在重写的方法的调用时,任何事情都可能发生,包括某些黑客利用堆栈溢出更改无关变量而进行的恶意接管。
但是,您应该使用const,避免使用全局变量,更喜欢使用指针的引用,避免将变量重新用于无关的任务等,这将使编译器在执行积极的优化时更轻松。
有多种方法可以解释这一点,其中之一是“ 停止问题”:
在可计算性理论中,停止问题可以表述为:“给出任意计算机程序的描述,确定程序是完成运行还是继续永远运行”。这等效于在给定程序和输入的情况下确定程序在使用该输入运行时最终将停止还是永久运行的问题。
艾伦·图灵(Alan Turing)在1936年证明,不存在解决所有可能的程序输入对暂停问题的通用算法。
如果我编写的程序看起来像这样:
do tons of complex stuff
if (condition on result of complex stuff)
{
change value of x
}
else
{
do not change value of x
}
x
改变的价值吗?为了确定这一点,您首先必须确定do tons of complex stuff
零件是否导致条件起火-或更基本的是,它是否停止。那是编译器无法做到的。
真的很惊讶没有直接使用暂停问题的答案!从这个问题到停止问题有非常直接的减少。
想象一下,编译器可以判断一个函数是否更改了变量的值。然后,假定可以在程序其余部分的所有调用中跟踪x的值,则肯定可以确定以下函数是否更改y的值:
foo(int x){
if(x)
y=1;
}
现在,对于我们喜欢的任何程序,让我们将其重写为:
int y;
main(){
int x;
...
run the program normally
...
foo(x);
}
请注意,当且仅当我们的程序更改y的值时,它才终止-foo()是退出前的最后一件事。这意味着我们已经解决了暂停问题!
上述减少表明,确定变量值是否发生变化的问题至少与停止问题一样困难。停止问题是众所周知的,因此这个问题也必须解决。
y
。在我看来就像foo()
迅速返回,然后main()
退出。(此外,您的通话foo()
毫无争议……这是我困惑的一部分。)
一旦一个函数调用了编译器没有“看到”源代码的另一个函数,它要么必须假定变量已更改,要么在下面可能会出错。例如,假设我们在“ foo.cpp”中包含以下内容:
void foo(int& x)
{
ifstream f("f.dat", ifstream::binary);
f.read((char *)&x, sizeof(x));
}
我们在“ bar.cpp”中有这个:
void bar(int& x)
{
foo(x);
}
编译器如何才能“知道” x
未更改的内容(或者更恰当地是进行更改)bar
?
我敢肯定,如果不够复杂,我们可以提出一些更复杂的建议。
const_cast
在foo中添加一个,它仍然会进行x
更改-我违反了合同,该合同规定您不要更改const
变量,但是由于您可以将任何内容转换为“更多const”并const_cast
存在,语言的设计师肯定会想到,有时有充分的理由相信const
价值可能需要改变。
正如已经指出的那样,编译器通常无法确定变量是否将被更改。
在检查常量性时,感兴趣的问题似乎是变量是否可以由函数更改。即使在支持指针的语言中也很难做到这一点。您无法控制其他代码对指针的作用,甚至可以从外部源读取它(尽管不太可能)。在限制访问内存的语言中,与C ++相比,这些类型的保证是可能的,并且允许更积极的优化。
为了使问题更具体,我建议书的作者可能想到以下限制条件:
在编译器设计的上下文中,我认为在代码生成正确性和/或代码优化的上下文中,假设1、3、4完全符合编译器编写者的观点。在没有volatile关键字的情况下,假设2是有意义的。而且这些假设也使问题足够集中,以使对拟议答案的判断更加明确:-)
给定这些假设,为什么不能假设恒定性的一个关键原因是变量别名。编译器无法知道另一个变量是否指向const变量。混叠可能是由于同一编译单元中的另一个函数引起的,在这种情况下,编译器可以查看函数并使用调用树来静态确定可能发生混叠。但是,如果别名是由于库或其他外来代码引起的,则编译器无法在函数输入时知道变量是否被别名。
您可能会争辩说,如果将变量/参数标记为const,则不应通过别名对其进行更改,但是对于编译器编写者来说,这是非常危险的。对于人类程序员来说,将变量const声明为一个大型项目(例如,他不知道整个系统或OS或库的行为,以至于真正知道变量将不知道)的一部分,甚至可能会有风险。改变。
即使声明了变量const
,也不意味着某些编写不正确的代码可以覆盖它。
// g++ -o foo foo.cc
#include <iostream>
void const_func(const int&a, int* b)
{
b[0] = 2;
b[1] = 2;
}
int main() {
int a = 1;
int b = 3;
std::cout << a << std::endl;
const_func(a,&b);
std::cout << a << std::endl;
}
输出:
1
2
a
和b
是堆栈变量,并且b[1]
恰好与处于相同的内存位置a
。
const
如果所有内容都被标记,为什么编译器无法弄清楚某些东西是否真实const
。这是因为未定义的行为是C / C ++的一部分。我试图寻找一种不同的方式来回答他的问题,而不是提及停顿问题或外部人为输入。
为了扩展我的意见,该书的案文尚不清楚,这使问题难以理解。
正如我评论的那样,这本书试图说:“让无数的猴子编写所有可能写的C ++函数。在某些情况下,如果我们选择一个变量(猴子编写的某些特定函数)使用时,我们无法确定函数是否会更改该变量。”
当然,对于任何给定应用程序中的某些(甚至许多)功能,这可以由编译器轻松确定。但不是所有(或大多数)。
可以很容易地对此功能进行分析:
static int global;
void foo()
{
}
“ foo”显然不会修改“ global”。它根本不会修改任何内容,并且编译器可以非常轻松地解决此问题。
此功能不能如此分析:
static int global;
int foo()
{
if ((rand() % 100) > 50)
{
global = 1;
}
return 1;
由于“ foo”的动作取决于在运行时可以更改的值,因此显然无法在编译时确定是否会修改“ global”。
整个概念比计算机科学家要理解的要容易得多。如果函数可以根据运行时可能发生的变化来做不同的事情,那么您将无法确定它要执行的操作,直到它运行,并且每次运行时它可能都会执行不同的操作。不管是否证明不可能,这显然都是不可能的。