web-dev-qa-db-ja.com

C / C ++でポインター記号と乗算記号が同じである理由

limited C/C++コードパーサーを作成しています。さて、乗算とポインタ記号はどちらも同じなので、本当に大変な時間になります。例えば、

_int main ()
{
  int foo(X * p); // forward declaration
  bar(x * y);  // function call
}
_

_*_が実際にポインターである場合、特別なルールを適用して整理する必要があります。上記のコードでは、foo()が前方宣言であり、bar()が関数呼び出しであるかどうかを確認する必要があります。実際のコードはもっと複雑になる可能性があります。ポインタに_@_のような別の記号があったとしたら、それは簡単だったでしょう。

ポインタはCで導入されましたが、同じシンボルに対していくつかの異なるシンボルが選択されなかったのはなぜですか?キーボードはそれほど制限されていませんでしたか?

[現代のパーサーがこれをどのように処理するかについて誰かが光を当てることができれば、それはアドオンになりますか?あるスコープではXをタイプ名にすることができ、別のスコープでは変数名にすることもできることに注意してください。]

18
iammilind

はい、同じシンボルが再利用されています。UTF32がそこに戻っていないからです。つまり、ポインタ型として*、逆参照演算子として*、乗算演算子として*があり、それはCでも同じです。たとえば、 "&"にも同様の問題があります( "&"はアドレスオフ、 "&として「ビット単位の終わり」および「&」は「&&」の一部として-論理and)、その他。

字句パーサーは、コンテキストに基づいてそれらを区別します。

この例では、パーサーに2つの異なるパスがあります。1つは型で始まるパス(変数/転送宣言)でなく、もう1つはパス(関数呼び出し)です。あいまいな場合は、コンパイルエラーが発生します。

Cのサブセットを使用している場合-この問題を処理する文法の適切なサブセットを取得していることを確認する必要があります。

17
littleadv

確かではありませんが、Cの発明者がキャラクターを使い果たした(またはほぼ使い果たした)ためです。

元々の意図は、言語が演算子に記号を使用する必要があり、言語がいわゆる「下位」内で表現可能でなければならないことでしたASCIIテーブルは、ASCII値0〜127。

最初の31個の値は印刷できない制御文字であるため、次のようにします。

!"#$%&'()*+,-./0123456789:;<=>?

_@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^__

`abcdefghijklmnopqrstuvwxyz {|}〜

スペース、および識別子に必要なすべての文字(すべて文字、すべての数字、およびアンダースコア)を削除すると、次のようになります。

!"#$%&'()*+,-./:;<=>?@[\]^ {|}〜

かっこ、中かっこ、角かっこ、および単一引用符との違いが十分でないバックティック( `)を削除すると、次のようになります。

_!"#$%&'*+,-./:;<=>?@\^|~_

言語の構文に重要な句読点( "# ',.:;?)を削除すると、次のようになります。

_!$%&*+-/<=>@\^|~_

アンパサンド(&)を削除します。これは、「のアドレスを取る」ことを意味し、したがって、ポインター、および比較(<=>)、感嘆符(!)に関連するものとは明確に異なる必要があります。プラス(+)とマイナス(-)は、ポインタに対して実行する有効な操作であり、次のようになります。

_$%*/@\^|~_

なぜドル記号とアットマークが考慮されなかったのかわかりません。おそらく、コード内で頻繁に使用するには少し大きすぎると考えられていました。だから、それらを取り出してください、そうすればこれらは残ります:

_%*/\^|~_

ご覧のとおり、上記の文字はすべてCのさまざまな算術演算および論理演算の演算子として使用されています。つまり、これらの文字の1つをポインターとして宣言および逆参照するための文字として再利用する必要がありました。

彼らはアスタリスク(*)を選択しました。これは、「ポイント」の概念をいくらか持っているため、および乗算はポインターに対して実行するのに有効な演算の1つではないため、適切な選択です。

彼らはキャレット(^)(これは排他的ORを意味します)を選択することもできましたが、それほど大きなことだとは思わず、オプションは非常に限られていました。

18
Mike Nakis

C BNFには、パーサーの作成を困難にする2つの規則があります。

if-statement: "if" expression statement |
              "if" expression statement "else" statement

dangling else 問題が発生します。

if a if b c else d

のいずれかとして解析できます

(if-statement (a) (if-statement (b) (c) (d)))
(if-statement (a) (if-statement (b) (c)) (d))

この競合の一般的な解決策は、reduceよりもshiftを優先することです。内部の(else)ブランチif-statement.

もう一つのより難しい問題は

typedef-name: identifier

これにより、言語は コンテキスト依存 になります。この競合は、パーサーでルールを省略し、typedef-namesの個別のトークンを作成することで解決されます。これを機能させるには、スキャナーにtypedefとして宣言されている名前のテーブルが必要です。

C++の場合、ルールははるかに複雑であり、通常、すべての識別子を解決する統合スキャナー/パーサーを作成する方が簡単です。

7
Simon Richter

シンボルテーブルを使用して解決されます。このシンボルテーブルは、すでに宣言されている関数を呼び出しているかどうかを分類するために見た宣言を追跡します。パーサーは、識別子を検出すると、シンボルテーブルを検索して、それが何であるかを確認します。 Cには複数の名前空間があり、それらのシャドウを開始できるため、型名についても同様の状況が見られます特に。これらのルールは重要です。

typedef int MahInt;
MahInt * p; // declaration or multiplication?

シンボルテーブルなしでCを解析することはできません。

なぜそうなのかについては、当時のキーボードは非常に限られていたためです。たとえば、「WTF」演算子を見てみましょう。

int x, y;
x ??!??! y;

それは本当に

x || y
4
DeadMG

typedefは、C言語への比較的後の追加でした。

Cの以前のバージョンでは、文法はタイプ名とは何か、そうでないものをかなり簡単に定義していました。多くの型名:intchardoubleなどは、単一のキーワードでした(現在も使用されています)。他のタイプ名にはキーワードまたは記号が含まれていました:struct foochar *int[42]。キーワード以外の識別子を型名にすることはできません。

typedef構成が既存の言語に追加されたとき、曖昧さを作成したり既存のコードを壊したりせずに、非キーワード識別子をタイプ名として扱うことができるように文法を変更することはできませんでした。たとえば、次の場所にあります。

int foo() {
    x*y;
}

x*yは、xyを乗算して結果を破棄する式ステートメント、またはyへのポインターとしてのxの宣言のいずれかです。

これを見る1つの方法は、typedefが新しいキーワードを作成することです。キーワードは、それが定義されているスコープの最後までのみ存在します。つまり、パーサーはシンボルテーブルを調べて、物事を解釈する方法を知る必要があります(他の多くの言語には当てはまらないものです。たとえば、Pascalでは、識別子は型名にすることができますが、これはあいまいさ)。

例えば:

int x, y;
int foo() {
     x * y; /* x isn't type name, so this is an expression statement */
     {
         typedef int x;
         x *y; /* Now x is a type name (effectively a keyword),
                  so this is a declaration */
     }
     x * y; /* The type name x is now out of scope,
               so it's an expression statement again */
}

(typedef名をキーワードとして扱うことは、それを見る1つの方法にすぎません。コンパイラーが実際にそれを内部的に行うことを示唆することを意味するものではありません。)

3
Keith Thompson

[〜#〜] glr [〜#〜] 解析の使用を検討してください。これにより、型情報が得られるまで、あいまいな構文解釈の間の選択を延期することができます。 Elsaのようなパーサーはこのように動作します。

別の方法は、解析中に型を収集して拡張することです。たとえば、アドホックな再帰的降下解析の実装でうまく機能します。このアプローチは、gccClangの両方で使用されます。

より多くの可能性があります:PEG仕様からPackratパーサーを生成する高レベルのジェネレーターを使用できます-この方法では、アドホックアプローチのように、全体を手動で実装する必要なく、より多くの副作用ロジックを解析に組み込むことができます。 。

1
SK-logic