演算子の優先順位が壊れています

Twitter でのディスカッションで、演算子の優先順位について考えさせられました。演算子の優先順位は、式の意味を決定するため、ほとんどのプログラミング言語の重要な部分です。

興味深いことに、これはほぼすべてのプログラミング言語で実質的に同じであり、根本的に確立された言語のより優れた代替手段になろうとするものでさえもです。

そうは思いません。演算子の優先順位には根本的な欠陥があり、簡単に改善できると思います。

この記事全体を通して C++ を例として使用しますが、これは従来の演算子を使用するすべてのプログラミング言語に当てはまります。

次のコードを検討してください:

x = a & b + c * d && e ^ f == 7;

そのコードを読んだらどう反応しますか?

あなたはおそらくそれを書いた人を責めるでしょう.

「括弧を使おう!」

「複数の小さな式にリファクタリングしてください!」

これは合理的な反応です。実際、この例は、避けるべき複雑な式に関するルールの C++ コア ガイドラインから取られています。

演算子の優先順位が明確でない場合は、ほとんどの言語で括弧を付けることが一般的なガイドラインです。コア ガイドラインをもう一度引用すると、すべての人が演算子の優先順位表を覚えているわけではありません。また、基本的な式を理解するために優先順位を覚える必要はありません。 .

ただし、悪いコードの作成者がここで本当に責任があるとは思いません。誰かがエディター/IDE を開いて、「今日は演算子の優先順位を乱用するつもりです。本当に」と考える人はおそらくまれです。

上記の例は意図的に極端なものですが、括弧の欠落について不満を述べたもっと合理的な例を考えてみてください。この演算子がそれよりも強力に結合することは作者にとって完全に明らかだったので、式は適切に形成されているのでしょうか?

演算子の優先順位は無作為に選択されたものではありません。その背後には特定のロジックがあります。

したがって、誰かが 2 つの演算子の相対的な優先順位を直感的に知っていて、そこに括弧が必要だとは思わなかったと予想できます。

本当の責任は、彼または彼女がそのようなひどい表現を書くことを可能にした言語にあると思います.人間の読者にとって潜在的にあいまいな表現を書くことを防がなければなりませんでした. .

誤解しないでください - 私は、プログラマーに可能な限り多くの自由を提供する言語に大賛成です.

しかし、判読できない式を書くメリットはありません。つまり、それを許可する理由はありません。

では、どのような演算子の優先順位が判読不能な式につながるのでしょうか?

オペレーターの優先順位の目標

演算子の優先順位が良いのはいつですか ?

達成すべき目標が 2 つあります。

1.演算子の優先順位は直感的であるべき

演算子の使用は、あらゆる種類の言語で非常に一般的な操作です。初心者から専門家まで、ほぼすべての人が演算子を使用するため、演算子を正しく使用することが絶対に重要です。

06 のような式を読むと 、誰でもそれが何をするかを推測できるはずです.そうでなければ、あなたの言葉は良くありません.

あなたの言語が一般的なイディオムから大幅に逸脱している場合、問題があります。16 の言語を想像してみてください。 23 です !いたるところにバグがあります。

あなたの言語のユーザーは、演算子の優先順位表を見るべきではありません。もしそうなら、それは失敗した設計です。

2.演算子の優先順位が役立つ

特定の演算子の一般的な使用法と相互作用がある場合、優先順位は「そのまま機能する」はずです。

かっこを常に使用しなければならない場合は、単純にメリットがありません。コードが乱雑になり、それを読む人をイライラさせるだけです。

C プログラミング言語 (および多くの派生言語) には、使用するたびに私を悩ませる「悪い優先順位」の好例があります。 、 46 , …) は低い 比較演算子のそれより (50 または「<」)。

理由はわかりませんが、この決定は嫌いです。

理由は簡単です:62 があるとします。 フラグの - 各列挙子には 1 つのビット セットがあり、ビットを設定することで、フラグの組み合わせを整数に格納します。したがって、フラグを設定するには、次のようにします。

unsigned flags;
flags |= enable_foo; // bitwise or to set

フラグが設定されているかどうかを確認するには、次のようにします。

// if there is any bit set in both flags and enable_foo,
// enable_foo is set
if (flags & enable_foo != 0)
    …

75 として解析されるため、これは間違ったことをします。 これは 82 です .

もう 1 つの一般的な例は、C++ のポインターからメンバーへの逆参照演算子 95 です。 .

メンバ変数へのポインタがある場合 102 113 を指定して逆参照したい 、あなたが書く:

auto value = obj.*mptr;

ただし、 128 の場合 メンバ関数へのポインタである場合、次のように記述する必要があります:

auto result = (obj.*mptr)(args);

はい、そうです 136 145 の結果で実際には何もできないので、これは特にばかげています。 - 呼び出す以外は!変数に格納することさえできません。

これらの演算子の優先順位は絶対に役に立たないので、異なるべきでした.

適切な演算子の優先順位はあり得ません

優れた演算子の優先順位の 2 つの目標を特定しました:直感的であることと、有用であることです。

しかし、問題があります。これら 2 つの目標は互いに矛盾しています。

バイナリ 154 を考えてみましょう 優先順位:168 を解析して修正する場合 177 として 、私たちは共通の規範から逸脱するでしょう.もっと便利なものを作成したでしょうが、それはまた直感的ではありません.

さらに、直感の領域は人によって異なります。

たとえば、181 は明らかです。 196 です 200 ではありません 、論理 211 として 論理および論理 221 の乗算として記述されます ただし、233 と記述すると一般的な C++ コンパイラの警告が表示されるという事実を考慮すると、 括弧がないと、一般的な知識とは思えません…

では、普遍的に直観的と考えられているものは何ですか?

<オール> <リ>

演算の数学的順序:244256 261 より強いバインド と 271 .誰もがここにいると思います.

<リ>

単項演算子は、二項演算子より強力に結合します。非常識です。 283 の場合 295 と解釈されます .しかし、私たちは - すでに! - メンバー関数へのポインターの例で示されているように、ここでグレーゾーンに到達します。ここで 304 が必要です。 319 になる 一方:これはメンバーへのポインターであり、これを使用するのは 322 などの実装者だけです。 または 336 、したがって、演算子 340 を犠牲にしても問題ありません そして、そのさらに常軌を逸した従兄弟演算子 353 .

<リ>

…実際にはそれだけです。他のすべては潜在的にあいまいです。

ただし、それに基づいて演算子の優先順位を実際に割り当てることはできず、すべての演算子に対して相対的な順序を選択する必要があります。

それとも…?

部分的に順序付けされた演算子

完全に順序付けられた演算子の優先順位を作成する代わりに、実際には必要ありません。 または 370 ほとんどの言語がそうであるように、これらの質問に答えようとしても、直観的な答えを出すことはできません。状況が非常に抽象的なため、 直感的な答えです。

386 のように、一緒に使用される演算子についても同様です。 と 398 - 直感的な操作を維持しながら相対的な優先順位を付けることは困難です。したがって、どちらか一方を選択する代わりに、何も選択しないこともできます。優先順位を同じにして、括弧なしで混合するとエラーになるようにします。

そして、連鎖するのが単純にばかげている演算子があります。

408 とは たとえば、3 つすべてが等しいかどうかはチェックしません。

418 とは

これらの式は、あなたが考えていることをしないので、実際には望んでいません.これらの式を書くことは役に立たないだけでなく、積極的に危険です. .したがって、それらの式を記述することは禁止されるべきです。

しかし、429 と書きたい場合はどうでしょうか。 ?

436 と書きたい場合 ?

445 の動作が本当に必要な場合はどうでしょうか。 ?

次に、括弧を使用します。

慎重に設計された演算子の優先順位が実施 「直感的でない場合は括弧を使用する」という一般的なガイドライン。コンパイラが単純に拒否するため、不明確な式を記述することは現在不可能です。

この種のアイデアに従うと、次のようになります。

演算子の最終優先順位

最も一般的な演算子だけを取り上げると、演算子の次の「カテゴリ」を特定できます。

    <リ>

    論理演算子:456462474

    <リ>

    比較演算子:484497508518 ¸ …

    <リ>

    算術演算子:二項/単項 527 および 532545 、および 553 .

    <リ>

    ビット演算子:560577587591607617

    <リ>

    関数呼び出し、配列添字、メンバー アクセスなどのその他の単項演算子

次の相対的な優先順位を割り当てることは理にかなっています:

単項演算子> 数学/ビット演算子> 比較演算子> 論理演算子

私が直観的であると考えたいくつかの仮定を超えて、いくつかの追加の仮定を行う必要があったことに注意してください.特に、 620 は C が行うことを行いません。しかし、この種の優先順位は依然として妥当であると思います。

数学/ビット単位の演算子の優先順位は同じですが、2 つのカテゴリを混在させると、相互に相対的な優先順位がないため、実際にはエラーになります。さらに、単項 632 最も優先度が高いですが、単項式と 646 のようなものしか想定していません は許可されていません。

カテゴリ内の演算子の相対的な優先順位は次のとおりです:

    <リ>

    論理演算子:659> 666 /674 、ただし混在していない 689693 チェーン

    <リ>

    比較演算子:連鎖なし

    <リ>

    算術演算子:単項 703 /718> 725 /738> 745 /756 、通常の結合性

    <リ>

    ビット演算子:単項 761 2 項演算子の前ですが、ここでも 776 の混合チェーンはありません 、 788797 シフト演算子の連鎖なし

    <リ>

    単項演算子:いつものように

次に、次の式はすべて整形式です:

a * b + c == foo & a
a && (!b || c)
array[a] + 32 < ~a | b

しかし、これらはそうではありません:

a & b + c
a << b + 1

結論

言語でそのような演算子の優先順位を使用すると、コンパイラがすべき式を拒否する言語が得られます かっこを使用しています。したがって、演算子を読みやすくするためにかっこを使用するという共通のガイドラインを適用しました。

実際にこれを行う言語を見つけることができませんでした。最も近いのは Pony で、any を混在させることは違法です かっこのない種類の演算子です。ただし、これは特に便利ではありません 演算子の優先順位。

すべてでガイドラインを静的に実施しながら ケースは通常はお勧めできません。ガイドラインです。 、結局のところ、ここに価値があると思います。最悪の場合、そうでなければかっこを書かなければならないでしょう.

これは良いことだと思います。