26

未定義の動作が原因で、多くの悪いことが発生し、発生し続けています (発生しないかどうかはわかりませんが、何かが発生する可能性があります)。これが導入されたのは、コンパイラが最適化する余地を残すためであり、C++ をさまざまなプラットフォームやアーキテクチャに簡単に移植できるようにするためでもあると理解しています。ただし、未定義の動作によって引き起こされる問題は、これらの議論によって正当化するには大きすぎるようです。未定義の動作の他の引数は何ですか? 存在しない場合、未定義の動作がまだ存在するのはなぜですか?

編集私の質問にいくつかの動機を追加するには:C ++の狡猾な同僚とのいくつかの悪い経験のために、私は自分のコードを可能な限り安全にすることに慣れてきました。すべての引数、厳密な const-correctness などをアサートします。私のコードを間違った方法で使用する余地はほとんどないので、そこから離れようとします。これまでの経験から、抜け穴があると人々がそれを使用し、私のコードが悪いと電話がかかってくることがわかっているからです。コードを可能な限り安全にすることは、良い習慣だと考えています。これが、未定義の動作が存在する理由を理解できない理由です。かなりのオーバーヘッドがなければ、実行時またはコンパイル時に検出できない未定義の動作の例を教えてください。

4

11 に答える 11

10

懸念の核心は、何よりも速度に関する C/C++ の哲学にあると思います。

これらの言語は、生の力がまばらで、何かを使用できるようにするためだけに可能な限りの最適化を行う必要があったときに作成されました。

UB の処理方法を指定するということは、最初にそれを検出してから、もちろん適切な処理を指定することを意味します。しかし、それを検出することは、言語の速度第一の哲学に反しています!

今日でも高速プログラムが必要ですか? はい、非常に限られたリソース (組み込みシステム) または非常に厳しい制約 (応答時間または 1 秒あたりのトランザクション) のいずれかで作業している私たちにとって、できる限り絞り込む必要があります。

私はモットーが問題により多くのハードウェアを投入することを知っています。私が働いているアプリケーションがあります:

  • 回答の予想時間は? 100 ミリ秒未満で、途中で DB 呼び出しが行われます (memcached に感謝します)。
  • 1 秒あたりのトランザクション数 ? 平均で 1200、ピークは 1500/1700 です。

それは約 40 のモンスターで動作します: 32GB の RAM を備えた 8 つのデュアルコア opteron (2800MHz)。現時点では、ハードウェアが増えると「高速化」することが難しくなるため、最適化されたコードと、それを可能にする言語が必要です (アセンブリ コードをそこに投入することは制限しました)。

とにかく、私はUBをあまり気にしないと言わなければなりません。プログラムが UB を呼び出すようになったら、実際に発生した動作を修正する必要があります。もちろん、すぐに報告された方が簡単に修正できます。それがデバッグ ビルドの目的です。

したがって、おそらく UB に焦点を当てるのではなく、言語の使い方を学ぶ必要があります。

  • 未チェックの呼び出しを使用しないでください
  • (専門家向け)チェックされていない呼び出しを使用しないでください
  • (教祖のために)あなたは本当にここでチェックされていない呼び出しが必要ですか?

そして、すべてが突然良くなります:)

于 2010-05-05T09:51:16.900 に答える
9

未定義の動作に対する私の見解は次のとおりです。

標準は、言語の使用方法と、正しい方法で使用されたときに実装がどのように反応するかを定義します。ただし、すべての機能のすべての可能な使用法をカバーするのは大変な作業になるため、標準ではそのままにしておきます。

ただし、コンパイラの実装では、「そのままにしておく」ことはできません。コードを機械語に変換する必要があり、空白の場所をそのままにしておくことはできません。多くの場合、コンパイラはエラーをスローできますが、常に実行できるとは限りません。プログラマが間違ったことを行っているかどうかを確認するために余分な作業が必要になる場合があります (たとえば、これを検出するためにデストラクタを 2 回呼び出すなど)。 、コンパイラは、特定の関数が呼び出された回数をカウントするか、余分な状態を追加するか、何かを追加する必要があります)。したがって、標準で定義されておらず、コンパイラがそれを許可するだけの場合、運が悪いと、機知に富んだことが起こることがあります。

于 2010-05-05T09:19:45.573 に答える
6

問題は未定義の動作によって引き起こされるのではなく、未定義の動作につながるコードを書くことによって引き起こされます。答えは簡単です - その種のコードを書かないでください - そうしないことは正確にはロケット科学ではありません。

はどうかと言うと:

かなりのオーバーヘッドがなければ実行時またはコンパイル時に検出できない未定義の動作の例

現実の問題:

int * p = new int;
// call loads of stuff which may create an alias to p called q
delete p;

// call more stuff, somewhere in which you do:
delete q;

コンパイル時にこれを検出することは不可能です。実行時には、単純に 2 番目の削除が未定義であると単純に言う場合よりも、非常に困難であり、メモリ割り当てシステムがはるかに多くのブックキーピング (つまり、遅くなり、より多くのメモリを消費する) を行う必要があります。これが気に入らない場合は、おそらく C++ はあなたに適した言語ではありません。Java に切り替えてみませんか?

于 2010-05-05T09:18:04.500 に答える
5

未定義の動作の主な原因はポインタです。そのため、C と C++ には多くの未定義の動作があります。

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

char * r = 0x012345ff;
std::cout << r;

このコードは非常に悪いように見えますが、エラーを発行する必要がありますか? そのアドレスが実際に読み取り可能な場合、つまり何らかの方法で取得した値 (デバイス アドレスなど) である場合はどうなりますか?

このような場合、操作が合法かどうかを知る方法はありません。そうでない場合、その動作はまったく予測不可能です。

これとは別に: 一般に、C++ は「ゼロ オーバーヘッド ルール」を念頭に置いて設計されているため ( The Design and Evolution of C++ を参照)、コーナー ケースなどをチェックするために実装に負担を課すことはできません。常に維持する必要があります。この言語は設計されており、実際にデスクトップだけでなく、リソースが限られている組み込みシステムでも使用されていることに注意してください。

于 2010-05-05T11:22:42.807 に答える
4

未定義の動作として定義されている多くのことは、コンパイラまたはランタイム環境による診断が不可能ではないにしても困難です。

簡単なものは、すでに定義済み- 未定義の動作になっています。純粋仮想メソッドの呼び出しを検討してください。これは未定義の動作ですが、ほとんどのコンパイラ/ランタイム環境では同じ条件でエラーが発生します:純粋仮想メソッドが呼び出されました。事実上の標準は、私が知っているすべての環境で、純粋仮想メソッド呼び出しを呼び出すと実行時エラーになるということです。

于 2010-05-05T09:20:34.310 に答える
3

The standard leaves "certain" behaviour undefined in order to allow a variety of implementations, without burdening those implementations with the overhead of detecting "certain" situations, or burdening the programmer with constraints required to prevent those situations arising in the first place.

There was a time when avoiding this overhead was a major advantage of C and C++ for a huge range of projects.

Computers are now several thousand times faster than they were when C was invented, and the overheads of things like checking array bounds all the time, or having a few megabytes of code to implement a sandboxed runtime, don't seem like a big deal for most projects. Furthermore, the cost of (e.g.) overrunning a buffer has increased by several factors, now that our programs handle many megabytes of potentially-malicious data per second.

It is therefore somewhat frustrating that there is no language which has all of C++'s useful features, and which in addition has the property that the behaviour of every program which compiles is defined (subject to implementation-specific behaviour). But only somewhat - it's not actually all that difficult in Java to write code whose behaviour is so confusing that from the POV of debugging, if not security, it might as well be undefined. It's also not at all difficult to write insecure Java code - it's just that the insecurity usually is limited to leaking sensitive information or granting incorrect privileges over the app, rather than giving up complete control of the OS process the JVM is running in.

So the way I see it is that good software engineering requires discipline in all languages, the difference is what happens when our discipline fails, and how much we're charged by other languages (in performance and footprint and C++ features you like) for insurance against that. If the insurance provided by some other language is worth it for your project, take it. If the features provided by C++ are worth paying for with the risk of undefined behaviour, take C++. I don't think there's much mileage in trying to argue, as if it was a global property that's the same for everyone, whether the benefits of C++ "justify" the costs. They're justified within the terms of reference for the design of the C++ language, which are that you don't pay for what you don't use. Hence, correct programs should not be made slower in order that incorrect programs get a useful error message instead of UB, and most of the time behaviour in unusual cases (e.g. << 32 of a 32-bit value) should not be defined (e.g. to result in 0) if that would require the unusual case to be checked for explicitly on hardware which the committee wants to support C++ "efficiently".

Look at another example: I don't think the performance benefits of Intel's professional C and C++ compiler justify the cost of buying it. Hence, I haven't bought it. Doesn't mean others will make the same calculation I made, or that I will always make the same calculation in future.

于 2010-05-05T10:50:28.840 に答える
2

コンパイラとプログラミング言語は、私のお気に入りのトピックの1つです。過去に私はコンパイラに関連するいくつかの調査を行いましたが、何度も未定義の動作を発見しました。

C++とJavaは非常に人気があります。それは彼らが素晴らしいデザインを持っているという意味ではありません。それらは、受け入れられるためだけに設計品質を損なうリスクを冒したため、広く使用されています。Javaは、ガベージコレクション、仮想マシン、およびポインターのない外観を実現しました。彼らは部分的に開拓者であり、以前の多くのプロジェクトから学ぶことができませんでした。

C ++の場合、主な目標の1つは、Cユーザーにオブジェクト指向プログラミングを提供することでした。Cプログラムでさえ、C++コンパイラでコンパイルする必要があります。それは多くの厄介なオープンポイントを作り、Cはすでに多くのあいまいさを持っていました。C ++の重点は、完全性ではなく、力と人気でした。多重継承を提供する言語は多くありませんが、C ++は、非常に洗練された方法ではありませんが、それを提供します。未定義の動作は、その栄光と下位互換性をサポートするために常に存在します。

堅牢で明確に定義された言語が本当に必要な場合は、別の場所を探す必要があります。悲しいことに、それはほとんどの人の主な関心事ではありません。たとえば、Adaは明確で定義された動作が重要な優れた言語ですが、ユーザーベースが狭いため、この言語を気にする人はほとんどいません。私はその言語が本当に好きなので、例に偏っています。ブログに何かを投稿しましたが、コンパイルする前でも言語定義がバグを減らすのにどのように役立つかについて詳しく知りたい場合は、これらのスライドをご覧ください。

私はC++が悪い言語だと言っているのではありません!目標はさまざまで、一緒に仕事をするのが大好きです。また、大規模なコミュニティ、優れたツール、およびSTL、Boost、QTなどのはるかに優れたものがあります。しかし、あなたの疑問は、優れたC++プログラマーになるための根源でもあります。C ++を使いこなせるようになりたい場合は、これが懸念事項の1つになるはずです。前のスライドとこの評論家を読むことをお勧めします。言語が期待どおりに機能していないときを理解するのに大いに役立ちます。

ところで。未定義の動作は、移植性に完全に反します。たとえば、Adaでは、データ構造のレイアウトを制御できます(CおよびC ++では、マシンとコンパイラに応じて変更できます)。スレッドは言語の一部です。したがって、CおよびC ++ソフトウェアを移植すると、喜びよりも苦痛が増します。

于 2010-05-05T10:14:13.193 に答える
2

未定義の動作と実装定義の動作の違いを明確にすることが重要です。実装で定義された動作により、コンパイラの作成者は、プラットフォームを活用するために言語に拡張機能を追加できます。このような拡張機能は、現実の世界で機能するコードを作成するために必要です。

一方、UBは、言語に大きな変更を加えたり、Cとの大きな違いを課したりせずにソリューションを設計することが困難または不可能な場合に存在します。これについて、BSが説明しているページからの1つの例は次のとおりです。

int a[10];
a[100] = 0; // range error
int* p = a;
// ...
p[100] = 0; // range error (unless we gave p a better value before that assignment)

範囲エラーはUBです。これはエラーですが、標準では定義できないため、プラットフォームがこれをどの程度正確に処理するかは標準では定義されていません。プラットフォームはそれぞれ異なります。言語の自動範囲チェックを含める必要があり、言語の機能セットに大幅な変更が必要になるため、エラーに設計することはできません。コンパイラーは実行時のサポートなしでは実際に何を指しているのかを知ることができないため、コンパイル時または実行時のいずれかで、言語が診断を生成するためのp[100] = 0エラーはさらに困難です。p

于 2010-05-05T13:16:51.797 に答える
1

私は数年前に同じ質問を自問しました。nullポインターに書き込む関数の動作を適切に定義しようとしたとき、私はすぐにそれを検討するのをやめました。

すべてのデバイスに保護されたメモリの概念があるわけではありません。そのため、セグメンテーション違反などを介してシステムを保護することはできません。すべてのデバイスが読み取り専用メモリを備えているわけではないため、書き込みが単に何もしないとは言えません。私が考えることができる他の唯一のオプションは、システムの助けなしにアプリケーションが例外を発生させる[または中止するなど]ことを要求することです。ただし、その場合、リストメモリの書き込み以降にポインタが変更されていないことを保証できない限り、コンパイラはすべてのメモリ書き込みの前にコードを挿入してnullをチェックする必要があります。それは明らかに受け入れられません。

したがって、「準拠したC ++コンパイラは、保護されたメモリを備えたプラットフォームにのみ実装できる」と言わずに、動作を未定義のままにしておくことが唯一の論理的な決定でした。

于 2010-05-05T09:36:29.470 に答える
1

これが私のお気に入りです:deleteそれを使用してnull以外のポインターを実行した後(逆参照だけでなく、キャストなども)は UB (see this question)です。

UB に遭遇する方法:

{
    char* pointer = new char[10];
    delete[] pointer;
    // some other code
    printf( "deleted %x\n", pointer );
}

これで、すべてのアーキテクチャで上記のコードが正常に動作することがわかりました。このような状況の分析を実行するようにコンパイラーまたはランタイムに教えることは、非常に困難で費用がかかります。場合によっては、ポインターとポインターの間で数百万行のコードになる場合があることを忘れないでくださいdelete。直後に null へのポインターを設定deleteするとコストがかかる可能性があるため、普遍的な解決策でもありません。

それがUBの概念がある理由です。コードに UB は必要ありません。多分うまくいかないかもしれません。この実装で動作し、別の実装で中断します。

于 2010-05-05T10:27:58.363 に答える
0

未定義の動作が良い場合があります。大きな整数を例にとってみましょう。

union BitInt
{
    __int64 Whole;
    struct
    {
        int Upper;
        int Lower; // or maybe it's lower upper. Depends on architecture
    } Parts;
};

仕様では、最後にWholeに対して読み取りまたは書き込みを行った場合、Partsからの読み取り/書き込みは定義されていません。

さて、それは私にはちょっとばかげています。なぜなら、私たちが組合の他の部分に触れることができなければ、そもそも組合を持つことに意味がないからですよね?

しかしとにかく、他の関数が2つの別々のintをとる間、いくつかの関数は__int64をとるでしょう。毎回変換するのではなく、このユニオンを使用できます。私が知っているすべてのコンパイラは、この未定義の動作をかなり明確な方法で処理します。したがって、私の意見では、未定義動作はここではそれほど悪くありません。

于 2010-05-05T09:32:10.243 に答える