C ++には「未定義の動作」(UB)があり、C#やJavaなどの他の言語にはないのはなぜですか?


50

このStack Overflowの投稿には、C / C ++言語仕様が「未定義の動作」であると宣言している状況のかなり包括的なリストがリストされています。ただし、C#やJavaなどの他の現代言語に「未定義の動作」という概念がない理由を理解したいと思います。つまり、コンパイラの設計者は、考えられるすべてのシナリオ(C#およびJava)を制御できるかどうか(CおよびC ++)を制御できますか?




3
それでも、このSO投稿は、Java仕様でも未定義の動作に言及しています!
gbjbaanb

「C ++に「未定義の動作」がある理由」残念ながら、これは「理由X、Y、および/またはZ(すべてがそうである可能性があるためnullptr)提案された仕様を作成および/または採用することにより、動作を定義することに煩わされました」。:c
code_dredd

前提に挑戦します。少なくともC#には「安全でない」コードがあります。Microsoftは「ある意味では、安全でないコードを書くことはC#プログラム内でCコードを書くことによく似ています」と書いており、そうする理由の例を挙げています。これが、Cが発明したものです(地獄、彼ら OSをCで書いたのです!)。
ピーター-モニカの復活

回答:


72

未定義の動作は、振り返ってみて非常に悪い考えとして認識されたものの1つです。

最初のコンパイラーは素晴らしい成果であり、機械語またはアセンブリー言語プログラミングの代替案に対する改善を喜んで歓迎しました。それに関する問題はよく知られており、これらの既知の問題を解決するために高級言語が特に発明されました。(当時の熱意は非常に大きかったので、HLLは「プログラミングの終わり」と呼ばれることもありました。これからは、私たちが望んでいることをささいに書き留めて、コンパイラがすべての実際の作業を行うようになります。)

新しいアプローチに伴う新しい問題に気付いたのは、後になってからです。コードが実行される実際のマシンから離れているということは、予期したとおりに動作しない可能性が静かにあることを意味します。たとえば、変数を割り当てると、通常、初期値は未定義のままになります。値を保持したくない場合は変数を割り当てないため、これは問題とは見なされませんでしたか?確かに、プロのプログラマーが初期値を割り当てることを忘れないことを期待することはあまりありませんでしたか?

より強力なプログラミングシステムで可能になった、より大きなコードベースとより複雑な構造により、多くのプログラマーがそのような見落としを時々犯すことが判明し、結果として生じる未定義の動作が大きな問題になりました。今日でも、小さなものから恐ろしいものまでのセキュリティリークの大部分は、何らかの形での未定義の動作の結果です。(その理由は、通常、未定義の動作は実際にはコンピューティングの次の下位レベルのものによって非常に定義されており、そのレベルを理解している攻撃者はプログラムを作成するためにその小刻みの部屋を使用して、意図しないものだけでなく、まさに物事を行うことができるからです彼らは意図している。)

これを認識してから、未定義の動作を高レベル言語から排除する一般的な動きがあり、Javaはこれについて特に徹底的でした(とにかく独自に設計された仮想マシンで実行するように設計されているため、比較的簡単でした)。Cのような古い言語は、膨大な量の既存のコードとの互換性を失わずに、そのように簡単に改造することはできません。

編集:指摘したように、効率性は別の理由です。未定義の動作とは、コンパイラの作成者がターゲットアーキテクチャを活用するための多くの余裕があり、各実装が各機能の可能な限り高速な実装で逃げることを意味します。これは、プログラマの給料がソフトウェア開発のボトルネックであることが多い今日よりも昨日の電力不足のマシンでより重要でした。


56
Cコミュニティの多くの人々がこの声明に同意するとは思わない。Cを後付けし、未定義の動作を定義する場合(たとえば、すべてをデフォルトで初期化する、関数パラメーターの評価順序を選択するなど)、適切に動作するコードの大規模なベースは引き続き完全に機能します。今日、適切に定義されていないコードのみが中断されます。一方、今日のままにしておくと、コンパイラはCPUアーキテクチャとコード最適化の新しい進歩を自由に活用できます。
クリストフ

13
答えの主な部分は、私にとって本当に説得力があるとは思えません。つまり、(int32_t add(int32_t x, int32_t y)++のように)C ++ で2つの数値を安全に追加する関数を書くことは基本的に不可能です。その周りの通常の議論は効率に関連していますが、多くの場合、いくつかの移植性の議論が散りばめられています(「一度書いて、実行した...あなたが書いたプラットフォームで...そしてそれ以外の場所で;-)」)。したがって、おおよそ、1つの引数は次のようになります
。16

12
@ Marco13合意-そして、「未定義の動作」の代わりに「未定義の動作」の問題を取り除くことは、「未定義の動作」ではなく「定義済みの動作ですが、必ずしもユーザーが望んだものではなく、発生時に警告なし」 。
アレフゼロ

9
「今日でも、小さなものから恐ろしいものへのセキュリティリークの大部分は、何らかの形での未定義の動作の結果です。」引用が必要です。私はそれらのほとんどが現在XYZ注入だと思っていました。
ジョシュア

34
「未定義の動作は、振り返ってみて非常に悪い考えとして認識されたものの1つです。」それはあなたの意見です。多く(自分自身を含む)は共有していません。
モニカとの軽量レース

103

基本的に、Javaおよび同様の言語の設計者は、言語で未定義の動作を望んでいないためです。これはトレードオフでした。未定義の動作を許可するとパフォーマンスが向上する可能性がありますが、言語設計者は安全性と予測可能性を優先しました。

たとえば、Cで配列を割り当てた場合、データは未定義です。Javaでは、すべてのバイトを0(または他の指定された値)に初期化する必要があります。つまり、ランタイムは配列を渡さなければならず(O(n)操作)、Cは即座に割り当てを実行できます。そのため、このような操作ではCは常に高速になります。

読む前に、配列を使用するコードが配列にデータを取り込む場合、これは基本的にJavaにとって無駄な努力です。ただし、コードが最初に読み取られる場合、Javaでは予測可能な結果が得られますが、Cでは予測できない結果が得られます。


19
HLLジレンマの優れたプレゼンテーション:安全性と使いやすさ対パフォーマンス。特効薬はありません。両側にユースケースがあります。
クリストフ

5
@Christophe公平を期すと、UBをCやC ++のように完全に無競争にするよりも、問題に対するはるかに優れたアプローチがあります。安全なマネージド言語を使用し、安全な領域にエスケープハッチを使用して、有益な場所に適用できます。TBH、C / C ++プログラムを「必要な高価なランタイムマシンを挿入します。私は気にしませんが、発生するすべてのUBについて教えてください」というフラグを付けてC / C ++プログラムをコンパイルできたら、本当にうれしいです。 」
アレクサンダー

4
初期化されていない場所を意図的に読み取るデータ構造の良い例は、Briggs and Torczonのスパースセット表現です(たとえば、codingplayground.blogspot.com / 2009/03 /…を参照 )。そのようなセットの初期化はCのO(1)ですが、O( n)Javaの強制初期化あり。
アーチD.ロビソン

9
データの初期化を強制すると破損したプログラムがはるかに予測可能になることは事実ですが、意図された動作を保証するものではありません:暗黙的に初期化されたゼロを誤って読み取る間にアルゴリズムが意味のあるデータを読み取ることを期待する場合、それはあたかもそれが持っていたかのようにバグですゴミを読む。C / C ++プログラムでは、このようなバグはでプロセスを実行すると表示されvalgrind、初期化されていない値が使用された場所を正確に示します。valgrindランタイムが初期化を実行し、valgrindsチェックが役に立たないため、Javaコードでは使用できません。
cmaster

5
@cmasterこれが、C#コンパイラが初期化されていないローカルからの読み取りを許可しない理由です。ランタイムチェックの必要はなく、初期化の必要もありません。コンパイル時の分析だけです。しかし、それでもトレードオフです-割り当てられていない可能性のあるローカルの分岐を処理する良い方法がない場合があります。実際には、そもそもこれが悪い設計ではなく、複雑な分岐(人間が解析するのが難しい)を避けるためにコードを再考することで解決した方が良いケースは見つかりませんでしたが、少なくとも可能です。
ルアーン

42

未定義の動作により、コンパイラに特定の境界または他の条件で奇妙または予期しない(または通常の)処理を行う自由度を与えることにより、大幅な最適化が可能になります。

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.htmlを参照してください

初期化されていない変数の使用:これは一般にCプログラムの問題の原因として知られ、これらをキャッチする多くのツールがあります:コンパイラの警告から静的および動的アナライザーまで。これにより、(Javaのように)すべての変数がスコープに入ったときにゼロで初期化する必要がなくなるため、パフォーマンスが向上します。ほとんどのスカラー変数の場合、これはオーバーヘッドをほとんど引き起こしませんが、スタック配列とmallocされたメモリはストレージのmemsetを被り、特にストレージは通常完全に上書きされるため、非常にコストがかかります。


符号付き整数オーバーフロー:「int」型の演算が(たとえば)オーバーフローした場合、結果は未定義です。たとえば、「INT_MAX + 1」がINT_MINであるとは限りません。この動作により、一部のコードにとって重要な特定のクラスの最適化が可能になります。たとえば、INT_MAX + 1が未定義であることを知っていると、「X + 1> X」を「true」に最適化できます。乗算のオーバーフローを「できない」ことを知ると(そうすることは未定義になるため)、「X * 2/2」から「X」への最適化が可能になります。これらは些細なことのように思えるかもしれませんが、これらの種類のものは一般にインライン化とマクロ展開によって公開されます。これが許可するより重要な最適化は、次のような「<=」ループ用です。

for (i = 0; i <= N; ++i) { ... }

このループでは、コンパイラーは、オーバーフロー時に「i」が未定義の場合、ループが正確にN + 1回繰り返されると想定できます。これにより、広範なループ最適化を開始できます。オーバーフローでラップアラウンドすると、コンパイラーはループが無限である可能性があると仮定する必要があります(NがINT_MAXの場合に発生します)-これらの重要なループ最適化を無効にします。多くのコードが誘導変数として「int」を使用するため、これは特に64ビットプラットフォームに影響します。


27
もちろん、符号付き整数のオーバーフローが未定義である本当の理由は、Cが開発されたときに、使用中の符号付き整数の少なくとも3つの異なる表現(1の補数、2の補数、符号の大きさ、おそらくオフセットバイナリ)があったことです、およびそれぞれがINT_MAX + 1に対して異なる結果を返します。オーバーフローを未定義にするa + badd b a、潜在的にコンパイラが他の形式の符号付き整数演算をシミュレートするのではなく、あらゆる状況でネイティブ命令にコンパイルできます。
マーク

2
整数オーバーフローが緩やかに定義された方法で動作することを許可すると、考えられるすべての動作がアプリケーションの要件を満たす場合に、大幅な最適化が可能になります。ただし、プログラマがすべてのコストで整数オーバーフローを回避する必要がある場合、これらの最適化のほとんどは没収されます。
supercat

5
@supercatこれは、未定義の動作を避けることが最近の言語でより一般的であるもう1つの理由です。プログラマーの時間はCPU時間よりもはるかに重要です。UBのおかげでCができる最適化の種類は、現代のデスクトップコンピューターでは本質的に無意味であり、コードに関する推論をより難しくします(セキュリティへの影響は言うまでもありません)。パフォーマンスが重要なコードであっても、Cではやや難しい(またはさらに難しい)高レベルの最適化の恩恵を受けることができます。私はC#で独自のソフトウェア3Dレンダラーを使用していますHashSet
ルアーン

2
@supercat:Wrt_loosely defined_、整数オーバーフローの論理的な選択は、実装定義の動作を要求することです。それは既存の概念であり、実装に対する過度の負担ではありません。ほとんどの場合、「ラップアラウンドで2の補数」で逃げるでしょう。<<難しい場合があります。
MSalters

@MSalters未定義の動作でも実装で定義された動作でもない、シンプルで十分に研究されたソリューションがあります。非決定的動作です。つまり、「x << y型の有効な値を評価しますが、int32_tどちらを言うのかはわかりません」と言うことができます。これにより、実装者は高速ソリューションを使用できますが、非決定性がこの1つの操作の出力に制約されるため、タイムトラベルスタイルの最適化を許可する誤った前提条件として機能しません-メモリ、揮発性変数などが目に見えて影響しないことを保証します式の評価によって。...
マリオカルネイロ

20

Cの初期には、多くの混乱がありました。コンパイラが異なれば、言語の扱いも異なります。言語の仕様を作成することに関心がある場合、その仕様は、プログラマーがコンパイラーに依存していたCとかなり後方互換性が必要です。ただし、これらの詳細の一部は移植性がなく、一般的には意味がありません。たとえば、特定のエンディアンやデータレイアウトを想定しています。したがって、C標準では、未定義の動作または実装指定の動作として多くの詳細が予約されており、コンパイラの作成者には多くの柔軟性が残されています。C ++はCに基づいて構築されており、未定義の動作も備えています。

Javaは、C ++よりもはるかに安全でシンプルな言語になろうとしました。Javaは、完全な仮想マシンに関して言語のセマンティクスを定義します。これにより、未定義の動作のためのスペースがほとんどなくなりますが、一方で、Java実装では困難な要件を作成します(たとえば、参照の割り当てはアトミックである必要がある、または整数の動作方法)。Javaが潜在的に安全でない操作をサポートする場合、それらは通常、実行時に仮想マシンによってチェックされます(たとえば、一部のキャスト)。


CとC ++が未定義の動作から抜け出せない唯一の理由は、後方互換性だけだと言っていますか?
シシル

3
それは間違いなく大きなものの1つ、@ Sisirです。経験豊富なプログラマーの間でさえ、コンパイラーが未定義の動作を処理する方法を変更すると、壊れてならないものがどれだけ壊れるかに驚くでしょう。(GCCが出て最適化し始めたときのケースがポイントで、混乱のビットがあった「でthisチェックはしばらく前に、という理由でnullの?」thisというのnullptrUBであり、したがって、実際に起こることはできません。)
ジャスティン時間2復職モニカ

9
@Sisir、もう一つの大きなものはスピードです。Cの初期には、ハードウェアは今日よりもはるかに異質でした。INT_MAXに1を追加したときに何が起こるかを単に指定しないことにより、アーキテクチャーで最速の処理をコンパイラに実行させることができます(たとえば、1の補数システムは-INT_MAXを生成し、2の補数システムはINT_MINを生成します)。同様に、配列の終わりを超えて読み取ったときに何が起こるかを指定しないことにより、メモリ保護を備えたシステムにプログラムを終了させることができます。
マーク

14

JVMおよび.NET言語は簡単です。

  1. ハードウェアを直接操作できる必要はありません。
  2. それらは、最新のデスクトップおよびサーバーシステム、または合理的に同様のデバイス、または少なくともそれらのために設計されたデバイスでのみ動作する必要があります。
  3. すべてのメモリにガベージコレクションを強制し、初期化を強制して、ポインタの安全性を確保できます。
  4. それらは、単一の決定的な実装も提供した単一のアクターによって指定されました。
  5. 彼らはパフォーマンスよりも安全を選択するようになります。

ただし、選択には良い点があります。

  1. システムプログラミングはまったく異なるものであり、代わりにアプリケーションプログラミングの妥協のない最適化が妥当です。
  2. 確かに、常にエキゾチックなハードウェアは少なくなっていますが、小さな組み込みシステムはここにあります。
  3. GCは、代替不可能なリソースには不向きであり、パフォーマンスを向上させるためにはるかに多くのスペースを使用します。また、ほとんどの(ほとんどすべてではない)強制的な初期化は、最適化して削除できます。
  4. より多くの競争には利点がありますが、委員会は妥協を意味します。
  5. これらのすべての境界チェック、ほとんどが最適化されて離れていても、合計されます。最適化はまだ禁止されていますが、ほとんどの場合、ヌルポインターチェックは、仮想アドレス空間のおかげでオーバーヘッドをゼロにするためにアクセスをトラップすることで実行できます。

エスケープハッチが提供される場合、それらは完全な未定義の振る舞いを呼び戻します。しかし、少なくともそれらは通常、ごく少数の非常に短いストレッチでのみ使用されるため、手動で確認する方が簡単です。


3
確かに。私は仕事のためにC#でプログラムしています。時々、危険なハンマーの1つにアクセスしunsafeます(のキーワードまたは属性System.Runtime.InteropServices)。管理されていないものをデバッグする方法を知っている少数のプログラマーにこのようなものを保持することで、再び実用的なものとして少しだけ、問題を抑えます。前回のパフォーマンス関連の安全でないハンマーから10年以上経ちましたが、文字通り他の解決策がないため、やらなければならないこともあります。
ジョシュア

19
私は頻繁にsizeof(char)== sizeof(short)== sizeof(int)== sizeof(float)== 1のアナログデバイスのプラットフォームで作業します。 、そしてCの良いところは、妥当なコードを生成する適合コンパイラを使用できることです。言語がラップアラウンドで2の補数を命じた場合、追加するたびにテストとブランチが発生します。これは、DSPに焦点を当てた部分のスターターではありません。これは現在の生産部品です。
ダン・ミルズ

5
@BenVoigt私たちの中には、小さなコンピューターが4kのコードスペース、固定の8レベルの呼び出し/リターンスタック、64バイトのRAM、1MHzクロック、1,000ドルでコストが0.20ドル未満の世界に住んでいる人もいます。現代の携帯電話は、すべての意図と目的のためにほぼ無制限のストレージを備えた小型のPCであり、ほぼPCとして扱うことができます。すべての世界がマルチコアであるとは限らず、厳しいリアルタイム制約がありません。
ダン・ミルズ

2
@DanMills:ここではArm Cortex Aプロセッサを搭載した最新の携帯電話については話しておらず、2002年頃の「機能電話」について話しています。はい192kBのSRAMは64バイト以上です(「小さい」ではなく「小さい」) 192kBは、30年間「正確な」デスクトップまたはサーバーと正確に呼ばれていません。また、最近では20セントで64バイトを超えるSRAMを搭載したMSP430を入手できます。
ベンフォイト

2
@BenVoigt 192kBは過去30年でデスクトップではないかもしれませんが、Webページを提供するのに十分であることを保証できます。これは、まさに言葉の定義によってそのようなものをサーバーにすると主張します。事実、それは多くの場合、構成Webサーバーを含む多くの組み込みアプリケーションにとって完全に妥当な(寛大な、均一な)RAM量であるということです。確かに、私はおそらくAmazonを実行していませんが、そのようなコアでIOTクラップスを備えた冷蔵庫を実行しているだけかもしれません(時間とスペースを空けて)。誰もそのためにインタプリタ言語またはJIT言語を必要としないでください!
ダン・ミルズ

8

JavaとC#の特徴は、少なくとも開発の初期段階にある主要ベンダーです。(それぞれSunとMicrosoft)。CとC ++は異なります。早くから複数の競合する実装がありました。Cは、特にエキゾチックなハードウェアプラットフォームでも実行されました。その結果、実装間にばらつきがありました。CとC ++を標準化したISO委員会は、大きな共通点に同意することができましたが、実装が異なる端では、標準が実装の余地を残していました。

これは、1つの動作を選択すると、別の選択に偏ったハードウェアアーキテクチャではコストがかかる可能性があるためです-エンディアンネスは明らかな選択です。


「大きな共通分母」とは文字通り何を意味するのでしょうか?サブセットまたはスーパーセットについて話しているのですか?本当に共通の十分な要因を意味しますか?これは最小公倍数または最大公約数のようなものですか?これは、通りの専門用語を話さず、数学だけを話す私たちのロボットにとって非常に紛らわしいです。:)
tchrist

@tchrist:一般的な動作はサブセットですが、このサブセットはかなり抽象的です。共通規格で指定されていない多くの分野では、実際の実装が選択を行わなければなりません。現在、これらの選択肢のいくつかは非常に明確であり、したがって実装定義ですが、他の選択肢はより曖昧です。が存在しなければならない:実行時のメモリレイアウトは一例である選択肢が、それはあなたがそれを文書化したいか明確ではありません。
MSalters

2
元のCは1人の男によって作成されました。設計上、すでに多くのUBがありました。Cが普及するにつれて事態は確実に悪化しましたが、UBは最初から存在していました。PascalとSmalltalkはUBがはるかに少なく、ほぼ同時に開発されました。Cの主な利点は、移植が非常に簡単だったことです。移植性の問題はすべてアプリケーションプログラマーに委任されました。LISPやSmalltalkのような何かをすることは、はるかに大きな努力でした(.NETランタイムのプロトタイプは限られていましたが:)。
ルアーン

@Luaan:それはカーニハンかリッチーでしょうか?いいえ、未定義の動作はありませんでした。机の上に元のAT&Tステンシルコンパイラドキュメントがありました。実装は、それがしたことをした。不特定の動作と未定義の動作に違いはありませんでした。
MSalters

4
@MSalters Ritchieが最初の男でした。Kernighanが参加したのは(それほどではない)後だった。まあ、「Undefined Behaviour」はありませんでした。その用語はまだ存在していなかったからです。しかし、今日では未定義と呼ばれるのと同じ動作をしていました。Cには仕様がなかったので、「不特定」でさえストレッチです:)これはコンパイラが気にかけなかったものであり、詳細はアプリケーションプログラマ次第でした。移植可能なアプリケーションを作成するために設計されたのではなく、移植しやすいコンパイラーのみが意図されていました。
ルアーン

6

本当の理由は、一方ではCとC ++、もう一方ではJavaとC#(ほんの2、3の例のみ)の意図の根本的な違いに帰着します。歴史的な理由から、ここでの議論の多くはC ++ではなくCについて述べていますが、(おそらくご存知のように)C ++はCのかなり直接的な子孫なので、Cについて言うことはC ++にも等しく当てはまります。

UNIXの大部分は忘れられていますが(その存在は時には否定されることもあります)、UNIXの最初のバージョンはアセンブリ言語で書かれていました。Cの当初の目的の大部分は(もしそうでないとしても)、UNIXをアセンブリ言語から高レベル言語に移植することでした。意図の一部は、可能な限り多くのオペレーティングシステムを高レベルの言語で記述すること、またはアセンブリ言語で書かなければならない量を最小限に抑えるために他の方向からそれを調べることでした。

それを実現するために、Cは、アセンブリ言語とほぼ同じレベルのハードウェアへのアクセスを提供する必要がありました。PDP-11は(一例として)I / Oレジスタを特定のアドレスにマップしました。たとえば、1つのメモリ位置を読み取って、システムコンソールでキーが押されたかどうかを確認します。読み取りを待機しているデータがあるときに、その場所に1ビットが設定されました。次に、指定された別の場所から1バイトを読み取り、押されたキーのASCIIコードを取得します。

同様に、一部のデータを印刷する場合は、指定した別の場所を確認し、出力デバイスの準備ができたら、指定した別の場所にデータを書き込みます。

このようなデバイスのドライバーの作成をサポートするために、Cでは、整数型を使用して任意の場所を指定し、ポインターに変換して、その場所をメモリ内で読み書きできます。

もちろん、これにはかなり深刻な問題があります。地球上のすべてのマシンのメモリが1970年代初期のPDP-11と同じようにレイアウトされているわけではありません。そのため、その整数を受け取ってポインターに変換し、そのポインターを介して読み取りまたは書き込みを行うと、取得しようとしているものについて誰も合理的な保証を提供できません。わかりやすい例として、読み取りと書き込みはハードウェア内の別々のレジスタにマッピングされる可能性があるため、何かを書き込んでからそれを読み直そうとすると、(通常のメモリとは対照的に)読んだ内容が書き込んだ内容と一致しない場合があります。

残る可能性がいくつかあります。

  1. 考えられるすべてのハードウェアへのインターフェースを定義します。何らかの方法でハードウェアとやり取りするために、読み取りまたは書き込みが必要なすべての場所の絶対アドレスを指定します。
  2. そのレベルのアクセスを禁止し、そのようなことをしたい人はだれでもアセンブリ言語を使用する必要があることを宣言します。
  3. 人々がそれを行うことを許可しますが、ターゲットとするハードウェアのマニュアルを読んで(たとえば)、使用しているハードウェアに合うようにコードを書くことは彼らに任せます。

これらのうち、1は十分に馬鹿げているように思われるので、さらに議論する価値はほとんどありません。2は基本的に言語の基本的な意図を捨てています。これにより、3番目のオプションは、本質的に合理的に検討できる唯一のオプションとなります。

かなり頻繁に発生する別のポイントは、整数型のサイズです。Cは、intアーキテクチャによって提案された自然なサイズであるはずの「位置」を取ります。したがって、32ビットVAXをプログラミングする場合、intおそらく32ビットである必要がありますが、36ビットUnivacをプログラミングする場合、intおそらく36ビット(など)である必要があります。サイズが8ビットの倍数であることが保証されている型のみを使用して36ビットコンピューターのオペレーティングシステムを記述することは、おそらく合理的ではありません(不可能な場合もあります)。たぶん私は表面的なだけかもしれませんが、36ビットマシン用のOSを書いているなら、おそらく36ビットタイプをサポートする言語を使いたいと思うようです。

言語の観点から、これはさらに未定義の動作につながります。32ビットに収まる最大値を取得した場合、1を追加するとどうなりますか?通常の32ビットハードウェアでは、ロールオーバー(または、何らかのハードウェア障害が発生する可能性があります)一方、36ビットのハードウェアで実行されている場合は、追加するだけです。言語がオペレーティングシステムの記述をサポートする場合、どちらの動作も保証できません。型のサイズとオーバーフローの動作の両方を異なるように許可する必要があります。

JavaとC#はそれらすべてを無視できます。オペレーティングシステムの記述をサポートすることを目的としていません。それらを使用すると、いくつかの選択肢があります。1つは、ハードウェアが要求するものをサポートするようにすることです。8、16、32、64ビットのタイプを要求するため、それらのサイズをサポートするハードウェアを構築するだけです。他の明らかな可能性は、基礎となるハードウェアが何を望んでいるかに関係なく、言語が彼らが望む環境を提供する他のソフトウェアの上でのみ動作することです。

ほとんどの場合、これは実際にはどちらかまたは両方の選択肢ではありません。むしろ、多くの実装は両方を少し実行します。通常、オペレーティングシステムで実行されているJVMでJavaを実行します。多くの場合、OSはCで、JVMはC ++で記述されています。JVMがARM CPUで実行されている場合、CPUにARMのJazelle拡張機能が含まれており、ハードウェアをJavaのニーズにより近づけるため、ソフトウェアで実行する必要が少なくなり、Javaコードの実行速度が速くなりますとにかくゆっくり)。

概要

CとC ++には未定義の動作があります。これは、誰もが意図したことを実行できる許容可能な代替手段を定義していないためです。C#とJavaは別のアプローチを取りますが、そのアプローチはCとC ++の目標に(もしあれば)不十分に適合します。特に、どちらも、arbitrarily意的に選択されたほとんどのハードウェア上でシステムソフトウェア(オペレーティングシステムなど)を作成するための合理的な方法を提供していないようです。どちらも通常、既存のシステムソフトウェア(通常はCまたはC ++で記述されている)が提供する機能に依存してジョブを実行します。


4

C規格の作成者は、読者が自明であると思ったものを認識することを期待し、公開された根拠でほのめかしましたが、率直に言ってはいません。なぜなら、顧客は委員会よりも自分のニーズをよく知っている必要があるからです。特定の種類のプラットフォームのコンパイラが特定の方法でコンストラクトを処理することが予想されることが明らかな場合、そのコンストラクトが未定義の動作を呼び出すとスタンダードが言うかどうかは誰も気にする必要はありません。規格に適合したコンパイラがコードの一部を有効に処理することを義務付けていないことは、プログラマがそうでないコンパイラを喜んで購入する必要があることを意味するものではありません。

言語設計に対するこのアプローチは、コンパイラの作成者が製品を有料の顧客に販売する必要がある世界では非常にうまく機能します。コンパイラの作者が市場の影響から孤立している世界では、完全にばらばらになります。1990年代に人気を博した言語を操作した方法で言語を操作するための適切な市場条件が存在することは疑わしく、健全な言語設計者がそのような市場条件に依存することをさらに疑います。


あなたはここで重要なことを説明したと感じますが、それは私を逃れます。答えを明確にできますか?特に2番目の段落:現在の条件と以前の条件は異なると書かれていますが、わかりません。正確に何が変わったのですか?また、「ウェイ」は以前とは異なります。これも説明しますか?
アナトリグ

4
キャンペーンは未定義のすべての動作を未指定の動作に置き換えるか、またはより制約のあるものが依然として強力になっているようです。
デデュプリケーター

1
@anatolyg:まだ公開していない場合は、公開されているC Rationaleドキュメント(GoogleでC99 Rationaleと入力)を読んでください。11ページ23〜29行目は「マーケットプレイス」について、13ページ5〜8行目は移植性に関して何が意図されているかについて説明しています。コンパイラライターが、標準で定義されていないアクションを実行するためにコードが「壊れた」と他のすべてのコンパイラが有効に処理しているとオプティマイザーがコードを壊したとプログラマーに言った場合、商業コンパイラ会社の上司はどのように反応すると思いますか?それが継続を促進するため、それをサポートすることを拒否しました...
supercat

1
...そのような構造の使用?このような視点は、clangとgccのサポートボードで容易に明らかになり、壊れた言語のgccとclangがサポートするよりもはるかに簡単かつ安全に最適化を促進できる組み込み関数の開発を妨げています。
スーパーキャット

1
@supercat:コンパイラベンダーに苦情を言っているのは無駄です。あなたの懸念を言語委員会に向けてみませんか?彼らがあなたに同意する場合、正誤表が発行され、それを使用してコンパイラチームを率いて打つことができます。そして、そのプロセスは、新しいバージョンの言語の開発よりもはるかに高速です。しかし、彼らが同意しない場合、あなたは少なくとも実際の理由を得るでしょう、一方コンパイラの作者は(何度も)繰り返します。「我々はコードが壊れていることを指定しませんでした。その決定は言語委員会によって行われました。彼らの決定に従ってください。」
ベンフォイト

3

C ++とcの両方に記述的な標準があります(とにかくISOバージョン)。

言語がどのように機能するかを説明し、言語が何であるかについて単一の参照を提供するためにのみ存在します。通常、コンパイラベンダーとライブラリライターが道をリードし、いくつかの提案が主要なISO標準に含まれます。

JavaとC#(またはVisual C#、私があなたが言うことを意味する)には規範的な標準があります。彼らは、事前に明確に言語に何が含まれているか、それがどのように機能するか、そして許可された行動とみなされるものを教えてくれます。

それよりも重要なことは、Javaには実際にOpen-JDKに「参照実装」があるということです。(RoslynはVisual C#の参照実装と見なされますが、そのソースは見つかりませんでした。)

Javaの場合、標準にあいまいさがあり、Open-JDKは特定の方法でそれを行います。Open-JDKが行う方法は標準です。


状況はそれよりも悪いです。委員会が記述的か規範的かについて合意を達成したことはないと思います。
スーパーキャット

1

未定義の動作により、コンパイラはさまざまな設計者に対して非常に効率的なコードを生成できます。エリックの答えは最適化に言及していますが、それはそれを超えています。

たとえば、符号付きオーバーフローはCの未定義の動作です。実際には、コンパイラはCPUが実行する単純な符号付き加算オペコードを生成することが期待されており、その動作は特定のCPUが実行するものです。

これにより、Cはほとんどのアーキテクチャで非常に優れたパフォーマンスを発揮し、非常にコンパクトなコードを生成できました。符号付き整数が特定の方法でオーバーフローする必要があると規格が指定している場合、異なる動作をするCPUは、単純な符号付き加算のために、より多くのコード生成を必要とします。

これが、Cの未定義の動作の多くの理由でintあり、サイズのようなものがシステム間で異なる理由です。Intは、アーキテクチャに依存し、一般に、より大きい最大で最も効率的なデータ型として選択されますchar

Cが新しい頃、これらの考慮事項は重要でした。コンピューターはそれほど強力ではなく、多くの場合、処理速度とメモリが制限されていました。Cはパフォーマンスが本当に重要な場所で使用され、開発者は、これらの未定義の動作が特定のシステムで実際にどのようなものになるかを知るのに十分なほどコンピューターが機能する方法を理解することが期待されていました。

JavaやC#などの最近の言語は、未処理のパフォーマンスよりも未定義の動作を排除することを好みました。


-5

ある意味では、Javaにもそれがあります。Arrays.sortに誤ったコンパレータを指定したとします。それの検出の例外をスローすることができます。それ以外の場合、特定のことを保証しない何らかの方法で配列をソートします。

同様に、複数のスレッドの変数を変更すると、結果も予測できなくなります。

C ++は、さらに定義されていない状況(または、むしろJavaがより多くの操作を定義することを決定した)を作り、その名前を付けるためにさらに進んでいます。


4
これは、ここで話しているような未定義の動作ではありません。「誤ったコンパレーター」には、合計順序を定義するものと定義しないものの2種類があります。アイテムの相対的な順序を一貫して定義するコンパレータを提供する場合、動作は明確に定義されており、プログラマが望んでいた動作ではありません。相対的な順序に関して一貫性のないコンパレーターを提供する場合、振る舞いは依然として明確に定義されています:ソート関数は例外をスローします(これはおそらくプログラマーが望んでいた振る舞いでもありません)。
マーク

2
変数の変更に関しては、競合状態は通常、未定義の動作とは見なされません。Javaが共有データへの割り当てを処理する方法の詳細は知りませんが、言語の一般的な哲学を知っているため、アトミックである必要があると確信しています。53または71を同時に割り当てるaことは、51または73を取得できれば未定義の動作になりますが、53または71しか取得できない場合は明確に定義されています。
マーク

@Markシステムのネイティブワードサイズ(たとえば、16ビットワードサイズシステムの32ビット変数)よりも大きいデータのチャンクでは、各16ビット部分を個別に格納する必要があるアーキテクチャを持つことができます。(SIMDは別の潜在的なそのような状況です。)その場合、単純なソースコードレベルの割り当てでさえ、それがアトミックに実行されることを保証するためにコンパイラによって特別な注意が払われない限り、必ずしもアトミックではありません。
CVn
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.