C ++関数が特定の変数の値を変更するかどうかを判断できるコンパイラーを構築できないのはなぜですか?


104

私は本でこの行を読みました:

C ++関数が特定の変数の値を変更するかどうかを実際に決定できるコンパイラを構築することはおそらく不可能です。

このパラグラフは、コンパイラがcons-nessをチェックするときに保守的である理由について話していました。

なぜそのようなコンパイラを構築することが不可能なのですか?

コンパイラーは常に、変数が再割り当てされているか、非const関数が呼び出されているか、または非constパラメーターとして渡されているかどうかを常にチェックできます...


24
最初に頭に浮かぶのは、ダイナミックリンクライブラリです。私が自分のマシンでコードをコンパイルし、あなたが自分のマシンでコードをコンパイルし、実行時にそれらをリンクする、コンパイラーは変数を変更したかどうかをどのようにして知ることができますか?
Mooing Duck 2013

4
@MooingDuckまさにこれ。より大まかに言えば、コンパイラーは関数を個別にコンパイルするのではなく、すべてをコンパイラーのスコープ内とは限らない、より広い範囲の一部としてコンパイルします。
Called2voyage 2013

3
「不可能」は誇張かもしれません-「計算的に実行不可能」(NP困難のように)はより良い特徴付けかもしれませんが、学生が理解するのは少し難しいです。リンクされたリストまたは他の抽象的なデータ構造を想像してください。そのリスト/ツリー/何でもノードを変更する関数を呼び出すと、コンパイラーは基本的にプログラムを完全にシミュレートせずに、変更されたノードを正確に証明することができます予想される入力。1つのソースファイルをコンパイルするのに3日はかかりませんが、
twalberg

36
@twalberg Impossibleは誇張ではありません。いくつかの回答で説明されているように、ここでは停止問題が適用されます。一般的なプログラムをアルゴリズム的に完全に分析することは不可能です。
Fiktik 2013

5
有効なプログラムのサブセットのみをコンパイルする@twalbergコンパイラはあまり役に立ちません。
カレブ2013

回答:


139

なぜそのようなコンパイラを構築することが不可能なのですか?

同じ理由で、特定のプログラムが終了するかどうかを決定するプログラムを作成することはできません。これは停止問題と呼ばれ、計算不可能な問題の1つです。

明確にするために、場合によっては関数が変数変更すると判断できるコンパイラを作成できますが、関数が変数を変更する(または停止する)かどうかを確実に通知するコンパイラを作成することはできません。すべての可能な機能。

ここに簡単な例があります:

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

コンパイラは、そのコードを見ただけで、foo今後変更されるaかどうかをどのように判断できますか?機能するかどうかは、関数の外部条件、つまりの実装に依存しますbar。停止の問題が計算できないことの証明にはそれ以上のものがありますが、リンクされているWikipediaの記事(およびすべての計算理論の教科書)ですでにうまく説明されているため、ここでは正しく説明しません。


48
@mrsoltys、量子コンピュータはいくつかの問題に対して「ただ」指数関数的に高速であり、それらは決定不可能な問題を解決することができません。
zch 2013

8
@mrsoltysこれらの指数的に複雑なアルゴリズム(因数分解など)は量子コンピューターに最適ですが、問題を停止することは論理的なジレンマです。
user1032613 2013

7
@mrsoltys、ただの賢者であるために、はい、それは変わるでしょう。残念ながら、それはアルゴリズムが終了していてまだ実行中であることを意味しますが、残念ながら、直接観察しないと実際の状態に影響を与えるため、どちらを使用するかを判断できません。
Nathan Ernst

9
@ThorbjørnRavnAndersen:OK、それで私がプログラムを実行しているとしましょう。それが終了するかどうかをどのように正確に判断しますか?
ruakh 2013

8
@ThorbjørnRavnAndersenしかし、実際にプログラムを実行し、それが終了しない場合(たとえば、無限ループ)、プログラムが終了しないことに気付くことはありません。最後の1つ...
MaxAxeHax 2013

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 2013

28
これは実際には、停止問題の決定不能性に関する有名な証明の優れた改作にすぎません。
Konstantin Weitz

10
この具体的なケースでは、 "modifies_variable"はtrueを返します。変数が実際に変更されている実行パスが少なくとも1つあります。そして、その実行パスは、外部の非決定的関数の呼び出し後に到達します。したがって、関数全体は非決定的です。これら2つの理由により、コンパイラーは悲観的な見方をして、変数を変更するかどうかを決定する必要があります。確定的比較(コンパイラーによって検証可能)の結果がfalse(つまり、「1 == 1」)になった後で変数を変更するパスに到達した場合、コンパイラーはそのような関数が変数を変更しないと安全に判断できます
Joe Pineda

6
@JoePineda:問題はf変数を変更できるかどうかであり、変数を変更できるかどうかではありません。この答えは正しいです。
Neil G

4
@JoePineda:modifies_variableコンパイラのソースからのコードをコピー/貼り付けることを妨げるものは何もありません。引数を完全に無効にします。(オープンソースを想定していますが、要点は明確である必要があります)
orlp

60

「これらの入力を与えられた変数を変更するかしないか」「変数を変更する実行パスがあるか」を混同しないでください

前者は不透明な述語決定と呼ばれ、決定するのは簡単ではありません-停止問題からの削減は別として、入力が不明なソース(ユーザーなど)からのものである可能性があることを指摘するだけで済みます。これは、C ++だけでなく、すべての言語に当てはまります。

ただし、後者のステートメントは、すべての最適化コンパイラーが行う構文解析ツリーを見れば判断できます。それらが行う理由は、純粋な関数 (および参照透過の一部の定義では参照透過関数)は、簡単にインライン化できる、またはコンパイル時に値が決定されるなど、適用できるあらゆる種類の優れた最適化を備えているためです。しかし、関数が純粋であるかどうかを知るに、変数を変更できるかどうかを知る必要があります。

したがって、C ++に関する意外なステートメントのように見えるのは、実際にはすべての言語についてのささいなステートメントです。


5
これは私にとって最良の答えです。その区別をすることが重要です。
UncleZeiv 2013

「取るに足りない」?
2013

2
@Kipの「決定するのは簡単ではない」は、おそらく「決定することが不可能であり、証明が簡単なこと」を意味します。
fredoverflow 2014年

28

「C ++関数が特定の変数の値を変更するかどうか」のキーワードは「意志」だと思います。C ++関数特定の変数の値変更できるかどうかをチェックするコンパイラーを構築することは確かに可能です。変更が発生することを確実に言うことはできません。

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

「C ++関数が特定の変数の値を変更できるかどうかをチェックするコンパイラを構築することは確かに可能です」いいえ、それは不可能です。カレブの答えを見てください。コンパイラがfoo()がaを変更できるかどうかを知るには、bar()が0を返すことが可能かどうかを知る必要があります。また、計算可能な関数のすべての可能な戻り値を通知できる計算可能な関数はありません。したがって、コンパイラーがそれらに到達するかどうかを判断できないようなコードパスが存在します。到達できないコードパスでのみ変数が変更された場合、変数は変更されませんが、コンパイラーはそれを検出しません
Martin Epsz

12
@MartinEpsz「できる」とは、「変更できる」ではなく、「変更できる」ことを意味します。私はこれがOPがconstネスチェックについて話すときに心に留めていたことだと信じています。
dasblinkenlight 2013

@dasblinkenlight私は、OPが最初のもの、「変更が許可される」、または「変更されるかどうかはわからない」ではなく「絶対に変更されない」を意味している可能性があることに同意する必要があります。もちろん、これが問題になるシナリオは考えられません。コンパイラを変更して、識別子を含むすべての関数または「変更可能」応答属性を持つ関数の呼び出しのいずれかに対して「変更可能」と応答するようにすることもできます。そうは言っても、CとC ++は物事の定義が非常に緩いため、これを試すには恐ろしい言語です。これが、C ++での一貫性が問題になる理由だと思います。
DDS 2013

@MartinEpsz:「計算可能な関数のすべての可能な戻り値を通知できる計算可能な関数はありません」。「考えられるすべての戻り値」をチェックするのは間違ったアプローチだと思います。方程式を解くことができる数学的なシステム(maxima、mathlab)があります。つまり、同様のアプローチを関数に適用することは理にかなっています。つまり、いくつかの未知数を含む方程式として扱います。問題は、フロー制御+副作用=>解決できない状況です。IMO、それら(関数型言語、割り当て/副作用なし)がなければ、どのパスプログラムが実行されるかを予測することができます
SigTerm

16

特定の関数が特定の変数を変更するかどうかをコンパイル時にアルゴリズムで知ることができないことを説明するために停止問題を呼び出す必要はないと思います。

代わりに、多くの場合、関数の動作はランタイム条件に依存することを指摘するだけで十分です。これは、コンパイラーが事前に知ることはできません。例えば

int y;

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

y変更されるかどうかをコンパイラが確実に予測するにはどうすればよいですか?


7

これは実行可能であり、コンパイラは一部の関数に対して常にそれを実行しています。これは、たとえば、単純なインラインアクセサまたは多くの純粋な関数の簡単な最適化です。

一般的なケースではそれを知ることは不可能です。

別のモジュールからのシステムコールや関数呼び出し、またはオーバーライドされる可能性のあるメソッドへの呼び出しがある場合は常に、ハッカーがスタックオーバーフローを使用して関係のない変数を変更することによる敵対的テイクオーバーが起こりました。

ただし、constを使用し、グローバルを回避し、ポインターへの参照を優先し、無関係なタスクに変数を再利用しないようにする必要があります。これにより、積極的な最適化を実行するときにコンパイラーのライフが容易になります。


1
それを正しく思い出せば、関数型プログラミングの要点ですね。純粋に確定的で副作用のない関数のみを使用することにより、コンパイラーは積極的な最適化、実行前、実行後、メモ化、さらにはコンパイル時の実行を自由に行うことができます。多くの回答者が無視している(または混乱している)と私が思う点は、すべてのプログラムの正常に動作するサブセットで実際に可能であるということです。そして、いいえ、このサブセットは些細なことでも興味をそそるものでもありません。実際、それは非常に便利です。しかし、それは絶対に一般的なケースでは実際に不可能です。
ジョーピネダ2013

オーバーロードはコンパイル時の概念です。おそらく「オーバーライドされたメソッド」を意味していました。
fredoverflow 2014年

@FredOverflow:はい、オーバーライドされます。オーバーロードは確かにコンパイル時の概念です。指摘してくれてありがとう(もちろん、実装が別のコンパイルユニットからのものである場合、コンパイラはそれを分析するのにまだ問題がある可能性がありますが、それは私が意図したものではありませんでした)。答えを直します。
クリス2014年

6

これを説明する方法は複数ありますが、その1つが停止問題です。

計算可能性理論では、停止の問題は次のように述べることができます:「任意のコンピュータープログラムの説明を与えられて、プログラムが実行を終了するか、永久に実行し続けるかを決定します」。これは、プログラムと入力が与えられた場合、プログラムがその入力で実行されると最終的に停止するか、それとも永久に実行されるかを決定する問題に相当します。

アランチューリングは、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()は終了する前に行う最後の処理です。これは、停止の問題を解決したことを意味します。

上記の削減が示すのは、変数の値が変化するかどうかを判断する問題が、少なくとも停止の問題と同じくらい難しいということです。停止の問題は計算不可能であることが知られているので、これもまたそうでなければなりません。


私のプログラムがの値を変更した場合にプログラムが終了する理由について、私があなたの推論に従っているかはわかりませんyfoo()すぐに戻ってmain()終了するように見えます。(また、あなたはfoo()議論なしで呼んでいます...それは私の混乱の一部です。)
LarsH

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ますか?

これが十分に複雑でなければ、もっと複雑なものを思いつくことができると私は確信しています。


コンパイラーは、バーxがconst-to-reference-to-constとして渡された場合、xがバーで変更されていないことを知ることができます。
クリケット選手2013

はい、しかしconst_castfooにaを追加した場合でも、x変更は行われます- const変数を変更しないことを示す契約に違反しますが、何でも「より多くのconst」に変換でき、const_cast存在します。言語の設計者は確かに、const値を変更する必要があるかもしれないと信じる正当な理由が時々あるという考えを心に留めていました。
Mats Petersson 2013

@MatsPetersson:const_castを使用すると、コンパイラーが破損する可能性があるため、破損したすべての部分を保持できると思いますが、それを補償する必要はありません。
Zan Lynx 2013

@ZanLynx:はい、それは正しいと確信しています。しかし同時に、キャストは存在します。つまり、言語を設計した人は、「いつかこれが必要になるかもしれない」というある種の考えを持っていることを意味します。
Mats Petersson 2013

1

指摘されているように、変数変更れるかどうかをコンパイラが判断することは、一般的に不可能です。

定数をチェックするとき、問題は変数関数によって変更できるかどうかであるようです。ポインタをサポートする言語では、これも難しいです。他のコードがポインターで行うことを制御することはできません。外部ソースから読み取ることもできます(可能性は低いですが)。メモリへのアクセスを制限する言語では、これらのタイプの保証が可能で、C ++よりも強力な最適化が可能です。


2
言語でサポートしてほしいことの1つは、一時的で、戻り可能で、持続可能な参照(またはポインター)の違いです。エフェメラル参照は他のエフェメラル参照にのみコピーでき、リターナブル参照はエフェメラルまたはリターナブル参照にコピーでき、永続化可能な参照はどの方法でもコピーできます。関数の戻り値は、「戻り可能」パラメーターとして渡される最も制限の多い引数によって制約されます。残念ながら、多くの言語では、参照を渡すときに、それが使用される期間を示すものは何もありません。
スーパーキャット2013

それは確かに役に立ちます。もちろんこれにはパターンがありますが、C ++(および他の多くの言語)では、常に「チート」が可能です。
Krumelur 2013

.NETがJavaよりも優れている主な方法は、それが一時参照の概念を持っていることですが、残念ながら、オブジェクトが一時参照としてプロパティを公開する方法はありません(私が本当に知りたいのは、プロパティを使用するコードは、オブジェクトを操作するために使用する必要がある一時的な参照を(一時的な変数とともに)コードに
渡し

1

質問をより具体的にするために、私は次の一連の制約が本の著者が考えていたかもしれないものであったかもしれないことを示唆します:

  1. コンパイラが変数の定数に関して特定の関数の動作を調べていると仮定します。正確さのために、関数が別の関数を呼び出した場合に変数が変更された場合、コンパイラーは(以下で説明するエイリアシングのため)仮定する必要があるため、仮定#1は関数呼び出しを行わないコードフラグメントにのみ適用されます。
  2. 変数が非同期または並行アクティビティによって変更されないと仮定します。
  3. コンパイラーが変数を変更できるかどうかのみを決定し、変数が変更されるかどうかは決定しないと想定します。つまり、コンパイラは静的分析のみを実行しています。
  4. コンパイラが正しく機能するコードのみを考慮していると想定します(配列のオーバーラン/アンダーラン、不正なポインタなどは考慮していません)。

コンパイラー設計のコンテキストでは、コードジェネレーションの正確性やコード最適化のコンテキストでのコンパイラーライターの観点から、仮定1、3、4は完全に理にかなっていると思います。前提条件2は、volatileキーワードがない場合に意味があります。そして、これらの仮定は、提案された回答の判断をより明確にするのに十分な質問にも焦点を当てています:-)

これらの仮定を前提として、定数が仮定できない主な理由は、変数のエイリアシングによるものです。コンパイラーは、別の変数がconst変数を指しているかどうかを知ることができません。エイリアシングは、同じコンパイル単位内の別の関数が原因である可能性があります。その場合、コンパイラは関数全体を調べ、呼び出しツリーを使用して、エイリアシングが発生する可能性があることを静的に判断できます。ただし、エイリアスがライブラリまたは他の外部コードによるものである場合、コンパイラは、関数のエントリ時に変数がエイリアスされているかどうかを知る方法がありません。

変数/引数がconstとマークされている場合、エイリアスによって変更されることはないはずですが、コンパイラライターにとってはかなり危険です。人間のプログラマーが変数constを、システム全体、OS、またはライブラリーの動作を知らない大規模なプロジェクトの一部として宣言することは、変数が実際にそうなることを本当に知っているため、危険な場合さえあるt変更します。


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
マークラカタ2013

1
-1。未定義の動作は、コンパイラの動作に対するすべての制限を取り除きます。
MSalters 2013

反対票については不明です。これはconst、すべてがラベル付けされている場合にコンパイラが何かが本当にそうであるかをコンパイラが理解できない理由についてのOPの元の質問の例にすぎませんconst。これは、未定義の動作がC / C ++の一部であるためです。私は彼の質問に答える別の方法を見つけようとしていたのではなく、停止の問題や外部からの人間の入力に言及するのではありませんでした。
マークラカタ2013

0

私のコメントを拡大すると、その本のテキストは問題を難読化するのが不明確です。

私がコメントしたように、その本はこう書こうとしている、「無限の数のサルに、これまでに記述される可能性のある考えられるすべてのC ++関数を記述させましょう。変数(サルが記述した特定の関数)を選択する場合があります。使用すると、関数がその変数を変更するかどうかを判断できません。」

もちろん、特定のアプリケーションの一部(多くでも)の関数の場合、これはコンパイラーによって簡単に決定できます。しかし、すべてではない(または必ずしもほとんどではない)。

この機能は簡単に分析できます:

static int global;

void foo()
{
}

「foo」は明らかに「グローバル」を変更しません。これは何も変更せず、コンパイラーはこれを非常に簡単に処理できます。

この関数はそれほど分析できません:

static int global;

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

「foo」のアクションは、実行時に変更れる可能性がある値に依存するため、「グローバル」を変更するかどうかはコンパイル時に決定することはできません。

この全体の概念は、コンピューター科学者が理解するよりも理解するのがはるかに簡単です。実行時に変更される可能性のある事柄に基づいて、関数が何か異なることを実行できる場合、関数が実行されるまで何を実行するかを計算することはできません。証明が不可能であろうとなかろうと、それは明らかに不可能です。


あなたが言うことは本当ですが、コンパイル時にすべてがわかっている非常に単純なプログラムであっても、プログラムが停止することすらできず、何も証明できません。これが停止の問題です。たとえば、Hailstoneシーケンスen.wikipedia.org/wiki/Collat​​z_conjectureに基づいてプログラムを記述し、1つに収束した場合にtrueを返すようにすることができます。コンパイラはそれを行うことができず(多くの場合オーバーフローするため)、数学者でさえ、それが本当かどうかわからない。
クリス

「何も証明できない非常に単純なプログラムがいくつかある」という意味であれば、私は完全に同意します。しかし、チューリングの古典的な停止問題の証明は、矛盾を設定するためにプログラム自体が停止するかどうかを判断できるプログラム自体に依存しています。これは数学なので実装ではありません。確かに、特定の変数が変更されるかどうか、およびプログラムが停止するかどうかをコンパイル時に静的に決定することは完全に可能です。数学的に証明できない場合もありますが、場合によっては実際に達成可能です。
El Zorko 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.