1. ホーム
  2. c++

[解決済み] オーバーロードされた && と || がショートしないのは、実は理由があるのでしょうか?

2022-06-01 22:11:22

質問

演算子の短絡挙動について &&|| はプログラマにとって素晴らしいツールです。

しかし、なぜオーバーロードするとこの動作が失われるのでしょうか?演算子は単に関数のための構文上の糖分であることは理解していますが、その演算子が bool の演算子にはこの動作があるのに、なぜこの単一の型に限定されなければならないのでしょうか?この背後にある技術的な理由があるのでしょうか?

どのように解決するのですか?

すべての設計プロセスは、相互に互換性のない目標間の妥協に帰結します。 残念ながら、オーバーロードされた && 演算子の設計過程では、最終的に混乱した結果が生じました。 && -- の特徴である短絡的な動作が省略されてしまうということです。

その設計プロセスがどのようにこの不幸な場所に行き着いたか、その詳細は私にはわかりません。しかし、後の設計プロセスがこの不運な結果をどのように考慮したかを見ることは、関連性があります。 C# では、オーバーロードされた && 演算子 を短絡させる。C#の設計者はどのようにしてそれを実現したのでしょうか?

他の回答の1つは、"lambda lifting"を提案しています。つまり

A && B

は道徳的に等価なものとして実現されうる。

operator_&& ( A, ()=> B )

ここで、2番目の引数は遅延評価のための何らかのメカニズムを使用し、評価されたときに、式の副作用と値が生成されるようにします。オーバーロードされた演算子の実装は、必要なときだけ遅延評価を行うことになります。

これは C# の設計チームが行ったことではありません。(余談ですが、ラムダリフティングは をするときにやったこと 式木表現 ?? 演算子で、特定の変換操作を遅延的に実行する必要があります。しかし、その詳細を説明することは大きな脱線になります。ラムダ リフティングは機能しますが、十分にヘビー級であるため、それを回避することを希望しました)。

むしろ、C# ソリューションは問題を 2 つの別々の問題に分解します。

  • 右側のオペランドを評価する必要があるか?
  • 上記の答えが "yes"であった場合、2 つのオペランドをどのように結合するのでしょうか?

したがって、この問題はオーバーロードを違法とすることで解決されます。 && を直接オーバーロードすることを違法とすることで問題は解決します。むしろ、C#では、オーバーロードする必要がある 演算子をオーバーロードする必要があります。

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(余談:実は3つあります。C#では、もし演算子 false が提供されている場合、演算子 true という質問に答える演算子も必要です。通常、このような演算子を1つだけ提供する理由はないため、C#は両方を要求します)。

形式のステートメントを考えてみましょう。

C cresult = cleft && cright;

コンパイラはこれを擬似C#で書いたかのようにコードを生成します。

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

見ての通り、左辺は常に評価されます。もしそれが "false-ish" であると判断されれば、それが結果となります。そうでなければ、右辺が評価され、その結果 を熱望 ユーザ定義演算子 & が呼び出されます。

|| 演算子は同様の方法で定義され、演算子trueの呼び出しとして、熱心な | 演算子の呼び出しとして定義されます。

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

4つの演算子すべてを定義することで -- true , false , &| -- C#では、単に cleft && cright だけでなく、短絡的でない cleft & cright も、また if (cleft) if (cright) ... とか c ? consequence : alternativewhile(c) といった具合に。

さて、すべての設計プロセスは妥協の産物であると申し上げました。ここでC#言語の設計者は、なんとか短絡的に &&|| のような、正しい値を設定する必要がありますが、そのためには 4 演算子の代わりに の代わりに演算子を使っているため、混乱する人もいるようです。演算子true/falseの機能は、C#の機能の中で最も理解されていない機能の1つです。C++ユーザーに馴染みのある、賢明で分かりやすい言語を目指すという目標に対して、短絡的な表現をしたいという要望や、ラムダリフティングなどの遅延評価を実装したくないという要望がありました。それはそれで妥当な妥協点だったと思いますが、重要なのは、それが は妥協の産物です。ちょうど とは異なる C++ の設計者がたどり着いた妥協点とは異なります。

このような演算子の言語設計の話題に興味があるなら、C# が null 可能なブール値に対してこれらの演算子を定義しない理由についての私のシリーズを読むことを検討してください。

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/