web-dev-qa-db-ja.com

C99プリプロセッサチューリングは完全ですか?

Boostプリプロセッサの機能 を発見した後、「C99プリプロセッサチューリングは完了しましたか?」

そうでない場合、資格がないために何が欠けていますか?

69
Anycorn

ここ は、プリプロセッサを悪用してチューリングマシンを実装する例です。プリプロセッサの出力を入力にフィードバックするには外部ビルドスクリプトが必要なので、プリプロセッサ自体はチューリング完全ではありません。それでも、それは興味深いプロジェクトです。

前にリンクされたプロジェクトの説明から:

プリプロセッサーはnotチューリング完了です。少なくとも、プログラムが1回だけ前処理されている場合はそうではありません。これは、プログラムにそれ自体を含めることが許可されている場合でも当てはまります。 (その理由は、特定のプログラムの場合、プリプロセッサには有限数の状態と、ファイルが含まれている場所で構成されるスタックしかありません。これはプッシュダウンオートマトンにすぎません。)

Paul Fultz IIの答えは非常に印象的で、プリプロセッサがこれまでに得ることができると思ったよりも確かに近いですが、それは本当のチューリングマシンではありません。 Cプリプロセッサには、無限のメモリと時間があったとしても、チューリングマシンのように任意のプログラムを実行できないようにする一定の制限があります。 C spec のセクション5.2.4.1は、Cコンパイラに次の最小制限を提供します。

  • 完全な式内の括弧で囲まれた式のネストレベル63
  • 内部識別子またはマクロ名の63の重要な最初の文字
  • 1つの前処理変換ユニットで同時に定義された4095マクロ識別子
  • 論理ソース行の4095文字

以下のカウンターメカニズムでは、値ごとにマクロ定義が必要であるため、マクロ定義の制限により、ループできる回数が制限されます(EVAL(REPEAT(4100, M, ~))は未定義の動作をもたらします)。これは基本的に、実行できるプログラムの複雑さを制限します。マルチレベル展開のネストと複雑さは、他の制限の1つにも影響を与える可能性があります。

これは、「無限メモリ」の制限とは根本的に異なります。この場合、仕様では、標準に準拠したCコンパイラは、時間やメモリなどが無限であっても、これらの制限に準拠することだけが要求されると明確に述べています。これらの制限を超える入力ファイルは、予測できない、または未定義の方法で処理できます。 (または完全に拒否されました)。一部の実装には、より高い制限があるか、まったく制限がない場合がありますが、これは「実装固有」と見なされ、標準の一部ではありません。 Paul Fultz IIの方法を使用して、チューリングマシンのようなものを有限の制限のない特定のコンパイラ実装に実装することは可能ですが、一般的には「これは、標準に準拠した任意のC99プリプロセッサで実行できる」という意味では、答えはノーです。ここでの制限は言語自体に組み込まれており、無限のコンピューターを構築できないという単なる副作用ではないため、チューリングの完全性が損なわれます。

31
bta

まあマクロは直接再帰的に拡張しませんが、これを回避する方法はいくつかあります。

プリプロセッサで再帰を実行する最も簡単な方法は、遅延式を使用することです。遅延式は、完全に展開するためにより多くのスキャンを必要とする式です。

#define EMPTY()
#define DEFER(id) id EMPTY()
#define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)()
#define EXPAND(...) __VA_ARGS__

#define A() 123
A() // Expands to 123
DEFER(A)() // Expands to A () because it requires one more scan to fully expand
EXPAND(DEFER(A)()) // Expands to 123, because the EXPAND macro forces another scan

何でこれが大切ですか?マクロがスキャンされて展開されると、無効化するコンテキストが作成されます。この無効化コンテキストにより、現在展開されているマクロを参照するトークンが青く塗られます。したがって、いったん青にペイントされると、マクロは拡張されなくなります。これが、マクロが再帰的に展開しない理由です。ただし、無効化コンテキストは1回のスキャン中にのみ存在するため、展開を延期することで、マクロが青く塗られるのを防ぐことができます。式にさらにスキャンを適用する必要があるだけです。これを行うには、次のEVALマクロを使用します。

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

ここで、再帰を使用してREPEATマクロを実装する場合、最初に状態を処理するためにいくつかの増分演算子と減分演算子が必要です。

#define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__)
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__

#define INC(x) PRIMITIVE_CAT(INC_, x)
#define INC_0 1
#define INC_1 2
#define INC_2 3
#define INC_3 4
#define INC_4 5
#define INC_5 6
#define INC_6 7
#define INC_7 8
#define INC_8 9
#define INC_9 9

#define DEC(x) PRIMITIVE_CAT(DEC_, x)
#define DEC_0 0
#define DEC_1 0
#define DEC_2 1
#define DEC_3 2
#define DEC_4 3
#define DEC_5 4
#define DEC_6 5
#define DEC_7 6
#define DEC_8 7
#define DEC_9 8

次に、ロジックを実行するためにさらにいくつかのマクロが必要です。

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 ~, 1,

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t

#define IF(c) IIF(BOOL(c))

#define EAT(...)
#define EXPAND(...) __VA_ARGS__
#define WHEN(c) IF(c)(EXPAND, EAT)

これらすべてのマクロを使用して、再帰的なREPEATマクロを作成できます。 REPEAT_INDIRECTマクロを使用して、それ自体を再帰的に参照します。これにより、マクロが別のスキャンで(および別の無効化コンテキストを使用して)展開されるため、マクロが青く塗られなくなります。ここではOBSTRUCTを使用しています。これにより、展開が2回延期されます。条件付きWHENはすでに1つのスキャンを適用しているため、これが必要です。

#define REPEAT(count, macro, ...) \
    WHEN(count) \
    ( \
        OBSTRUCT(REPEAT_INDIRECT) () \
        ( \
            DEC(count), macro, __VA_ARGS__ \
        ) \
        OBSTRUCT(macro) \
        ( \
            DEC(count), __VA_ARGS__ \
        ) \
    )
#define REPEAT_INDIRECT() REPEAT

//An example of using this macro
#define M(i, _) i
EVAL(REPEAT(8, M, ~)) // 0 1 2 3 4 5 6 7

カウンタの制限により、この例では10回の繰り返しに制限されています。コンピューターのリピートカウンターが有限のメモリによって制限されるのと同じように。コンピュータと同じように、複数のリピートカウンタを組み合わせて、この制限を回避できます。さらに、FOREVERマクロを定義できます。

#define FOREVER() \
    ? \
    DEFER(FOREVER_INDIRECT) () ()
#define FOREVER_INDIRECT() FOREVER
// Outputs question marks forever
EVAL(FOREVER())

これは?を永久に出力しようとしますが、適用されるスキャンがないため、最終的に停止します。ここで問題は、無限のスキャン数を指定した場合、このアルゴリズムは完了するでしょうか?これは停止問題として知られており、停止問題の決定不能性を証明するにはチューリング完全性が必要です。ご覧のとおり、プリプロセッサはチューリング完全言語として機能できますが、コンピュータの有限メモリに制限される代わりに、適用されるスキャンの有限数によって制限されます。

131
Paul Fultz II

完全なチューリングであるためには、決して終わらないかもしれない再帰を定義する必要があります-それらを呼び出す mu-recursive operator

そのような演算子を定義するには、定義済みの識別子の無限空間が必要です(各識別子が有限回数評価される場合)。先験的にアプリオリ結果が見つかる時間の上限。コード内に有限数の演算子があるため、無限の可能性をチェックできる必要があります。

したがって、このクラスの関数は、Cプリプロセッサでは計算できません。これは、Cプリプロセッサでは定義されたマクロの数が限られていて、それぞれが1回だけ展開されるためです。

Cプリプロセッサは Dave Prosserのアルゴリズム を使用します(1984年にWG14チームのためにDave Prosserによって作成されました)。このアルゴリズムでは、マクロは最初の展開の瞬間に青く塗られます。再帰呼び出し(または相互再帰呼び出し)では、最初の展開が開始された時点ですでに青色に塗られているため、展開されません。したがって、有限数の前処理行では、関数(マクロ)を無限に呼び出すことはできません。これは、mu再帰演算子の特徴です。

Cプリプロセッサは sigma-recursive operator のみを計算できます。

詳細は Marvin L.Minsky(1967)-Computation:Finite and Infinite Machines 、Prentice-Hall、Inc. Englewood Cliffs、N.J.などの計算過程を参照してください。

5
alinsoar

それは制限内で完全なチューリングです(無限のRAMを持たないすべてのコンピューターと同様に)。 Boost Preprocessor でできることを確認してください。

質問の編集に応じて編集する:

Boostの主な制限は、コンパイラ固有の最大マクロ展開深度です。また、再帰を実装するマクロ(FOR ...、ENUM ...など)は、実際には再帰的ではなく、ほぼ同一のマクロのおかげで、そのように表示されます。全体像を見ると、この制限は実際に再帰的な言語で最大スタックサイズを設定することと同じです。

制限されたチューリング完全性(チューリング互換性?)に本当に必要なのは、反復/再帰(同等の構成)と条件付き分岐の2つだけです。

4
Cogwheel