为什么不可能build立一个编译器来确定一个C ++函数是否会改变一个特定variables的值?

我在一本书中读到这一行:

build立一个能真正确定C ++函数是否会改变特定variables的值的编译器是不可能的。

这个段落是为了说明为什么编译器在检查一致性时是保守的。

为什么不可能build立这样的编译器?

编译器总是可以检查variables是否被重新分配,非const函数被调用,还是作为非const参数被传入…

为什么不可能build立这样的编译器?

出于同样的原因,你不能写一个程序,将确定是否任何给定的程序将终止。 这被称为暂停问题 ,这是不可计算的东西之一。

要清楚的是,你可以写一个编译器,它可以确定函数在某些情况下确实会改变variables,但是你不能编写一个可靠地告诉你函数将会或者不会改变variables(或者停止)每个可能的function。

这是一个简单的例子:

void foo() { if (bar() == 0) this->a = 1; } 

一个编译器如何确定,只要看看那个代码, foo是否会改变a ? 无论是否依赖于function外部的条件,即执行bar 。 还有更多的证据表明暂停问题是不可计算的,但在链接的维基百科文章(以及每个计算理论教科书中)已经很好地解释了这一点,所以我不会在这里尝试正确地解释它。

想象一下这样的编译器存在。 我们还假定为了方便起见,它提供了一个库函数,如果传递的函数修改给定的variables,返回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; } 

不要混淆“将或不会修改给定这些input的variables”,因为“有执行path修改variables”。

前者被称为不透明谓词确定 ,除了减less停止问题之外,还可以指出input可能来自未知来源(例如用户),这是不可能的。 所有的语言都是如此,而不仅仅是C ++。

但是,后一种说法可以通过查看分析树来确定,这是所有优化编译器都做的事情。 他们这样做的原因是, 纯函数 (对于一些引用透明的定义而言, 引用透明的函数)具有各种可以应用的优化优化,比如易于编译或者在编译时确定它们的值; 但要知道一个函数是否纯粹,我们需要知道它是否可以修改一个variables。

所以,关于C ++似乎是一个令人惊讶的陈述实际上是关于所有语言的简单陈述。

我认为“C ++函数是否会改变特定variables的值”这个关键词是“will”。 当然可以构build一个编译器来检查一个C ++函数是否被允许改变一个特定variables的值,但是不能肯定地说这个改变将会发生:

 void maybe(int& val) { cout << "Should I change value? [Y/N] >"; string reply; cin >> reply; if (reply == "Y") { val = 42; } } 

我不认为有必要调用暂停问题来解释你不能在编译时通过algorithm知道一个给定的函数是否会修改某个variables。

相反,指出一个函数的行为通常取决于运行时的条件,这是编译器事先不知道的。 例如

 int y; int main(int argc, char *argv[]) { if (argc > 2) y++; } 

编译器如何可以肯定地预测y是否会被修改?

它可以完成,编译器一直在做一些function ,例如对简单的内联访问器或许多纯函数的简单优化。

在一般情况下,不可能知道这一点。

每当有一个系统调用或函数调用来自另一个模块,或者调用一个潜在的overriden方法时,任何事情都可能发生,包括从某些黑客使用堆栈溢出的恶意接pipe来改变一个不相关的variables。

然而,你应该使用const,避免使用全局variables,更喜欢指向指针,避免重复使用不相关任务的variables等等,这将使得编译器在执行积极的优化时更加容易。

有多种途径可以解释这一点,其中之一就是停止问题 :

在可计算性理论中,暂停问题可以表述如下:“给定一个任意计算机程序的描述,决定程序是否运行完毕或者继续运行”。 这相当于在给定程序和input的情况下决定程序是否最终在input运行时停止或者将永远运行的问题。

Alan Turing在1936年certificate了解决所有可能的程序input对的暂停问题的一般algorithm是不存在的。

如果我写一个看起来像这样的程序:

 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部分导致条件开火 – 甚至更基本的是否停止。 这是编译器无法做到的事情。

真的很惊讶,没有直接使用停止问题的答案! 从这个问题到停止问题有一个非常简单的减less。

想象一下,编译器可以判断一个函数是否改变了一个variables的值。 那么它肯定能够判断下面的函数是否改变了y的值,假设x的值可以在整个程序的所有调用中被跟踪:

 foo(int x){ if(x) y=1; } 

现在,对于我们喜欢的任何程序,我们将其重写为:

 int y; main(){ int x; ... run the program normally ... foo(x); } 

请注意,如果并且只有当我们的程序改变了y的值时,它才会终止–foo()是退出前最后一件事情。 这意味着我们已经解决了暂停问题!

上述的减less表明,确定一个variables值是否变化的问题至less和停机问题一样困难。 停止问题已知是难以置信的,所以这个也是必须的。

只要一个函数调用另一个编译器不能“看见”源的函数,它就不得不假定variables被改变,否则事情可能会在下面进一步出错。 例如,假设我们在“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); } 

编译器如何“知道” xbar没有改变(或更改,更恰当)?

如果这个复杂程度不够,我相信我们可以想出更复杂的东西。

正如已经指出的那样,编译器通常不可能确定variables是否将被改变。

当检查常量时,感兴趣的问题似乎是如果variables可以被函数改变。 即使这在支持指针的语言中也很难。 你不能控制其他代码如何使用指针,它甚至可以从外部源读取(尽pipe不太可能)。 在限制访问内存的语言中,这些types的保证是可能的,并且允许比C ++更积极的优化。

为了使这个问题更加具体,我build议下面这组约束可能是本书的作者可能想到的:

  1. 假定编译器正在检查特定函数关于variables的常量的行为。 为了正确性,编译器必须假设(因为如下所述的别名)如果函数调用另一个函数variables变化,所以假设#1只适用于不做函数调用的代码片段。
  2. 假定该variables未被asynchronous或并发活动修改。
  3. 假设编译器只确定variables是否可以修改,而不是修改。 换句话说,编译器只执行静态分析。
  4. 假定编译器只考虑正确运行的代码(不考虑数组超出/欠载,坏指针等)

在编译器devise的上下文中,我认为假设1,3,4在代码生成器正确性和/或代码优化的背景下对于编译器编写者来说是非常有意义的。 假设2在没有volatile关键字的情况下是有意义的。 而这些假设也把这个问题集中在足以使判断一个提议的答案更加明确的问题上:-)

鉴于这些假设,无法假定恒定性的一个关键原因是由于可变的混叠。 编译器无法知道另一个variables是否指向constvariables。 别名可能是由于同一编译单元中的另一个函数造成的,在这种情况下,编译器可以查看函数并使用调用树来静态确定可能发生别名。 但是,如果别名是由于库或其他外部代码引起的,那么编译器无法知道函数input是否使用别名。

你可以争辩说,如果一个variables/参数被标记为const,那么它不应该通过别名来改变,但是对于一个风险很大的编译器来说。 对于一个程序员来说,把一个variablesconst声明为一个大项目的一部分甚至是有风险的,他不知道整个系统或者操作系统或者一个图书馆的行为,改变。

即使一个variables被声明为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 

为了扩大我的意见,这本书的文字是不清楚的混淆这个问题。

正如我所评论的那样,这本书试图说:“让我们得到无数的猴子来编写每一个可以写的C ++函数,如果我们select一个variables(猴子写的某个函数)使用,我们不能确定函数是否会改变这个variables。“

当然,对于任何给定应用程序中的一些(甚至很多)函数,这可以由编译器来确定,而且很容易。 但不是所有(或者最多)。

这个function可以很容易地分析:

 static int global; void foo() { } 

“富”显然不会修改“全球”。 它根本不会修改任何东西,编译器可以很容易地解决这个问题。

这个function不能被如此分析:

 static int global; int foo() { if ((rand() % 100) > 50) { global = 1; } return 1; 

由于“foo”的动作取决于运行时可以改变的值,因此在编译时显然不能确定是否修改“global”。

这整个概念比计算机科学家所理解的要简单得多。 如果函数可以在运行时做一些不同的事情,那么在运行之前,你不能确定它会做什么,每次运行它都可能会有所不同。 无论是否可行,显然是不可能的。