为什么不可能构建可以确定C ++函数是否会更改特定变量的值的编译器?


104

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

事实证明,构建真正可以确定C ++函数是否会更改特定变量值的编译器是不可能的。

该段讨论的是为什么在检查常量性时编译器为什么会保守。

为什么不可能构建这样的编译器?

编译器始终可以检查是否重新分配了变量,是否在其上调用了非常量函数或是否将其作为非常量参数传入...


24
我想到的第一件事是动态链接库。如果我在计算机上编译代码,而您在计算机上编译代码,并且我们在运行时将它们链接起来,那么您的编译器如何知道我是否修改了变量?
Mooing Duck 2013年

4
@MooingDuck正是这个。更广泛地讲,编译器不会单独编译函数,而是将其编译为更广泛的视图的一部分,而这可能并不全都在编译器的范围之内。
称为2voyage

3
“不可能”可能是一个夸大的说法-“在计算上不可行”(如NP-hard)可能是更好的表征,但对学生来说则难一些。想象一下一个链表或其他抽象数据结构。如果我调用一个函数来更改列表/树/任何目录中的一个节点,那么编译器将如何能够希望在没有完全用程序完全模拟程序的情况下确切地证明修改了哪个节点(也许更重要的是,哪个节点没有修改)。预期的输入,而无需花费3天的时间来编译一个源文件...
twalberg

36
@twalberg Impossible并不是夸大其词,因为几个答案都说明了停止问题在这里适用。在算法上完全不可能分析通用程序。
Fiktik

5
只编译有效程序子集的@twalberg编译器不是很有用。
Caleb

Answers:


139

为什么不可能构建这样的编译器?

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

明确地说,您可以编写可以确定某个函数确实在某些情况下更改了变量的编译器,但是您不能编写能够可靠地告诉您该函数将或不会更改变量(或暂停)的编译器。每个可能的功能。

这是一个简单的例子:

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

仅查看代码,编译器如何确定是否foo会改变a?是否执行取决于函数外部的条件,即的实现bar。停止问题不是可计算的,这还不止于此,但在链接的Wikipedia文章(以及每本计算理论教科书)中已经很好地说明了暂停问题,因此在这里我不会尝试正确地解释它。


48
@mrsoltys,量子计算机对于某些问题“成倍地”增长,无法解决不确定的问题。
zch 2013年

8
@mrsoltys那些指数级复杂的算法(例如分解)非常适合量子计算机,但是停顿问题是一个逻辑难题,无论您拥有哪种“计算机”都无法计算。
user1032613

7
@mrsoltys,只是个聪明人,是的,它会改变。不幸的是,这将意味着该算法已终止并且仍在运行,不幸的是,如果不直接观察,您将无法确定哪个会影响实际状态。
内森·恩斯特

9
@ThorbjørnRavnAndersen:好的,假设我正在执行一个程序。我如何确定它是否将终止?
ruakh

8
@ThorbjørnRavnAndersen但是,如果您实际执行该程序,并且它没有终止(例如,无限循环),您将永远不会发现它没有终止……您只需继续执行另一步,因为它可能会最后一个...
MaxAxeHax

124

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

12
真好!在我是一个悖论书面由程序员。
Krumelur

28
实际上,这只是对停顿问题的不确定性的著名证明的很好的改编。
康斯坦丁·威茨

10
在这种具体情况下,“ modify_variable”应返回true:至少有一个确实修改了变量的执行路径。并且在调用外部不确定性函数之后就到达了执行路径-因此整个函数都是不确定性的。由于这两个原因,编译器应采取悲观的观点,并决定确实要修改变量。如果在确定性比较(可由编译器验证)得出假(即“ 1 == 1”)之后到达修改变量的路径,则编译器可以放心地说该函数从不修改变量
Joe Pineda

6
@JoePineda:问题是是否f修改变量,而不是是否可以修改变量。这个答案是正确的。
Neil G

4
@JoePineda:没有什么可以阻止我从modifies_variable编译器源复制/粘贴代码,从而完全使您的参数无效。(假设开源,但要点很明确)
orlp

60

不要混淆“在给定这些输入的情况下是否会修改变量”,因为“具有修改变量的执行路径”。

前者称为不透明谓词确定,并且几乎无法决定-除了减少停顿问题外,您还可以指出输入可能来自未知来源(例如,用户)。这是真正的所有语言,而不仅仅是C ++。

但是,可以通过查看分析树来确定后一条语句,这是所有优化编译器都可以执行的操作。它们这样做的原因是,纯函数 (以及对于透明参照的定义,是透明参照函数)具有各种可以应用的不错的优化,例如易于使用或在编译时确定其值。但是要知道一个函数是否是纯函数,我们需要知道它是否可以修改变量。

因此,关于C ++的令人惊讶的陈述实际上是关于所有语言的琐碎陈述。


5
恕我直言,这是最好的答案,这一点很重要。
UncleZeiv

“根本不可能”?
Kip

2
@Kip“根本不可能决定”可能意味着“不可能决定,并且证明很琐碎”。
fredoverflow

28

我认为“ C ++函数是否会更改特定变量的值”中的关键词是“将”。当然可以构建一个编译器来检查是否允许 C ++函数更改特定变量的值,您不能确定地说更改即将发生:

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

“当然可以构建一个检查C ++函数是否可以更改特定变量的值的编译器。”不,不是。请参阅Caleb的答案。为了使编译器知道foo()是否可以更改a,就必须知道bar()是否有可能返回0。并且没有可计算函数可以告诉任何可计算函数所有可能的返回值。因此,存在一些代码路径,使编译器无法判断它们是否会到达。如果仅在无法访问的代码路径中更改了变量,则不会更改,但是编译器将无法检测到该
变量

12
@MartinEpsz“可以”是指“允许更改”,而不是“可以更改”。我相信这就是OP在谈论const-ness检查时所想到的。
dasblinkenlight

@dasblinkenlight我必须同意,我认为OP可能意味着第一个,“允许o更改”,或“可以或不可以更改”与“绝对不会更改”。当然,我不认为这是一个问题。您甚至可以修改编译器,以对包含标识符或具有“可能更改”答案属性的函数的调用的任何函数简单地回答“可能更改”。也就是说,C和C ++是令人讨厌的语言,因为它们对事物的定义如此松散。我认为这就是为什么const-ness完全是C ++中的问题。
DDS

@MartinEpsz:“并且没有可计算的函数可以告诉任何可计算函数的所有可能的返回值”。我认为检查“所有可能的返回值”是不正确的方法。有一些数学系统(maxima,mathlab)可以求解方程,这意味着将类似的方法应用于函数将很有意义。即将其视为具有多个未知数的方程式。问题是流量控制+副作用=>无法解决的情况。IMO,如果没有这些(功能性语言,没有赋值/副作用),则可以预测采用哪种路径程序
SigTerm

16

我认为没有必要调用暂停问题来解释在编译时无法通过算法得知给定函数是否会修改某个变量的问题。

相反,指出一个函数的行为通常取决于运行时条件就足够了,而编译器无法事先知道这些条件。例如

int y;

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

编译器如何确定y将要修改的内容?


7

可以做到这一点,并且编译器一直在为某些函数做这些事情,例如,这对于简单的内联访问器或许多纯函数来说是微不足道的优化。

在一般情况下是不可能知道的。

每当有来自另一个模块的系统调用或函数调用,或对潜在重写的方法的调用时,任何事情都可能发生,包括某些黑客利用堆栈溢出更改无关变量而进行的恶意接管。

但是,您应该使用const,避免使用全局变量,更喜欢使用指针的引用,避免将变量重新用于无关的任务等,这将使编译器在执行积极的优化时更轻松。


1
如果我没记错的话,那就是函数式编程的全部重点,对吗?通过仅使用确定性函数,没有副作用函数,编译器可以自由地进行积极的优化,执行前,执行后,记忆甚至在编译时执行。我认为许多回答者都忽略(或困惑)这一点是,对于所有程序而言,行为良好的子集确实确实有可能。不,这个子集并非无关紧要或无趣的,实际上它非常有用。但是对于绝对的一般情况,确实是不可能的。
乔·皮内达

重载是一个编译时概念。您可能的意思是“重写方法”。
fredoverflow 2014年

@FredOverflow:是的,我的意思是重写。重载确实是一个编译时概念。感谢您发现它(当然,如果实现来自另一个编译单元,编译器在分析它时仍然会遇到麻烦,但这不是我的意思)。我将解决答案。
kriss 2014年

6

有多种方法可以解释这一点,其中之一是“ 停止问题”

在可计算性理论中,停止问题可以表述为:“给出任意计算机程序的描述,确定程序是完成运行还是继续永远运行”。这等效于在给定程序和输入的情况下确定程序在使用该输入运行时最终将停止还是永久运行的问题。

艾伦·图灵(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零件是否导致条件起火-或更基本的是,它是否停止。那是编译器无法做到的。


6

真的很惊讶没有直接使用暂停问题的答案!从这个问题到停止问题有非常直接的减少。

想象一下,编译器可以判断一个函数是否更改了变量的值。然后,假定可以在程序其余部分的所有调用中跟踪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()毫无争议……这是我困惑的一部分。)
LarsH 2013年

1
@LarsH:如果修改后的程序终止,则它调用的最后一个函数是f。如果y被修改,则f被调用(其他语句不能更改y,因为它仅由修改引入)。因此,如果y被修改,则程序终止。
MSalters 2013年

4

一旦一个函数调用了编译器没有“看到”源代码的另一个函数,它要么必须假定变量已更改,要么在下面可能会出错。例如,假设我们在“ 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

我敢肯定,如果不够复杂,我们可以提出一些更复杂的建议。


如果将bar x作为引用传递给const,编译器可以知道x在bar中没有变化,对吗?
板球

是的,但是如果我const_cast在foo中添加一个,它仍然会进行x更改-我违反了合同,该合同规定您不要更改const变量,但是由于您可以将任何内容转换为“更多const”并const_cast存在,语言的设计师肯定会想到,有时有充分的理由相信const价值可能需要改变。
Mats Petersson

@MatsPetersson:我相信,如果您使用const_cast,则可以保留所有损坏的部分,因为编译器可以但不必对此进行补偿。
Zan Lynx

@ZanLynx:是的,我确定是正确的。但是同时,类型转换确实存在,这意味着设计该语言的人确实有某种观念,即“我们可能会在某个时候需要它” –这意味着它根本不做任何有用的事情。
Mats Petersson

1

正如已经指出的那样,编译器通常无法确定变量是否将被更改。

在检查常量性时,感兴趣的问题似乎是变量是否可以由函数更改。即使在支持指针的语言中也很难做到这一点。您无法控制其他代码对指针的作用,甚至可以从外部源读取它(尽管不太可能)。在限制访问内存的语言中,与C ++相比,这些类型的保证是可能的,并且允许更积极的优化。


2
我希望在语言中得到支持的一件事是区分临时引用,可返回引用和可持久引用(或指针)。临时引用只能复制到其他临时引用,可返回的引用可以复制到临时或可返回的引用,而持久性引用可以通过任何方式复制。函数的返回值将受到作为“可返回”参数传递的参数中最严格的约束。我认为很遗憾,在许多语言中,当一个引用通过时,没有任何迹象表明可以使用多长时间。
超级猫

那肯定是有用的。当然有一些模式,但是在C ++(和许多其他语言)中,总是有可能“作弊”。
Krumelur

.NET优于Java的主要方式是它具有临时引用的概念,但是不幸的是,对象无法将属性公开为临时引用(我真正希望看到的是一种通过哪个使用属性的代码会将短暂的引用传递给应该用于操作该对象的代码(以及临时变量)
。– supercat

1

为了使问题更具体,我建议书的作者可能想到以下限制条件:

  1. 假设编译器正在检查特定函数相对于变量恒定性的行为。为了正确起见,编译器将不得不假设(由于别名的原因,如下所述)如果该函数调用了另一个函数,则该变量已更改,因此假设#1仅适用于不进行函数调用的代码片段。
  2. 假设该变量未被异步或并发活动修改。
  3. 假设编译器仅确定变量是否可以修改,而不确定是否将其修改。换句话说,编译器仅执行静态分析。
  4. 假设编译器仅考虑功能正常的代码(不考虑数组溢出/欠载,错误的指针等)

在编译器设计的上下文中,我认为在代码生成正确性和/或代码优化的上下文中,假设1、3、4完全符合编译器编写者的观点。在没有volatile关键字的情况下,假设2是有意义的。而且这些假设也使问题足够集中,以使对拟议答案的判断更加明确:-)

给定这些假设,为什么不能假设恒定性的一个关键原因是变量别名。编译器无法知道另一个变量是否指向const变量。混叠可能是由于同一编译单元中的另一个函数引起的,在这种情况下,编译器可以查看函数并使用调用树来静态确定可能发生混叠。但是,如果别名是由于库或其他外来代码引起的,则编译器无法在函数输入时知道变量是否被别名。

您可能会争辩说,如果将变量/参数标记为const,则不应通过别名对其进行更改,但是对于编译器编写者来说,这是非常危险的。对于人类程序员来说,将变量const声明为一个大型项目(例如,他不知道整个系统或OS或库的行为,以至于真正知道变量将不知道)的一部分,甚至可能会有风险。改变。


0

即使声明了变量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

发生这种情况是因为ab是堆栈变量,并且b[1]恰好与处于相同的内存位置a
马克·拉卡塔

1
-1。未定义行为消除了对编译器行为的所有限制。
MSalters

不确定投下反对票。这只是OP最初提出的问题的一个示例,该问题是:const如果所有内容都被标记,为什么编译器无法弄清楚某些东西是否真实const。这是因为未定义的行为是C / C ++的一部分。我试图寻找一种不同的方式来回答他的问题,而不是提及停顿问题或外部人为输入。
马克·拉卡塔

0

为了扩展我的意见,该书的案文尚不清楚,这使问题难以理解。

正如我评论的那样,这本书试图说:“让无数的猴子编写所有可能写的C ++函数。在某些情况下,如果我们选择一个变量(猴子编写的某些特定函数)使用时,我们无法确定函数是否会更改该变量。”

当然,对于任何给定应用程序中的某些(甚至许多)功能,这可以由编译器轻松确定。但不是所有(或大多数)。

可以很容易地对此功能进行分析:

static int global;

void foo()
{
}

“ foo”显然不会修改“ global”。它根本不会修改任何内容,并且编译器可以非常轻松地解决此问题。

此功能不能如此分析:

static int global;

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

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

整个概念比计算机科学家要理解的要容易得多。如果函数可以根据运行时可能发生的变化来做不同的事情,那么您将无法确定它要执行的操作,直到它运行,并且每次运行时它可能都会执行不同的操作。不管是否证明不可能,这显然都是不可能的。


您说的是正确的,但是即使对于非常简单的程序,在编译时就知道了所有内容,您将无法证明任何内容,甚至程序也不会停止。这是停顿的问题。例如,您可以基于冰雹序列en.wikipedia.org/wiki/Collat​​z_conjecture编写一个程序,如果收敛到一个,则使其返回true。编译器将无法执行此操作(因为在许多情况下会溢出),甚至数学家也不知道它是否正确。
kriss

如果您的意思是“ 有些简单的程序您无法证明任何东西”,我完全同意。但是,图灵经典的“中止问题”证明基本上依赖于程序本身能够判断是否中止以建立矛盾。因为这不是数学,而是执行。当然,存在某些程序,完全有可能在编译时静态确定特定变量是否将被修改以及程序是否将暂停。它可能无法在数学上证明,但在某些情况下实际上是可以实现的。
El Zorko
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.