CおよびC ++の未定義の動作とは何ですか?不特定の動作と実装定義の動作はどうですか?それらの違いは何ですか?
CおよびC ++の未定義の動作とは何ですか?不特定の動作と実装定義の動作はどうですか?それらの違いは何ですか?
回答:
未定義の動作は、CおよびC ++言語の側面の1つであり、他の言語から来たプログラマーにとっては驚くべきことです(他の言語はそれをよりうまく隠そうとします)。基本的に、多くのC ++コンパイラはプログラムのエラーを報告しませんが、予測可能な動作をしないC ++プログラムを作成することは可能です!
古典的な例を見てみましょう:
#include <iostream>
int main()
{
char* p = "hello!\n"; // yes I know, deprecated conversion
p[0] = 'y';
p[5] = 'w';
std::cout << p;
}
変数p
は文字列リテラルを指し、"hello!\n"
以下の2つの割り当てはその文字列リテラルを変更しようとします。このプログラムは何をしますか?C ++標準のセクション2.14.5パラグラフ11によれば、未定義の動作を呼び出します。
文字列リテラルを変更しようとした場合の影響は定義されていません。
「待ってください、これで問題なくコンパイルして出力を得ることができますyellow
」または「未定義の文字列リテラルが読み取り専用メモリに格納されているため、最初の割り当ての試行でコアダンプが発生します」という叫び声が聞こえます。これは、未定義の動作の問題です。基本的に、この標準では、未定義の動作(鼻の悪魔でさえ)を呼び出すと、何でも起こるようになります。言語のメンタルモデルに従って「正しい」動作がある場合、そのモデルは単に間違っています。C ++標準には唯一の投票期間があります。
未定義の動作の他の例としては、境界を超えた配列へのアクセス、nullポインターの逆参照、存続期間が終了した後のオブジェクトへのアクセス、またはのような巧妙な式の書き込みなどがありi++ + ++i
ます。
C ++標準のセクション1.9では、未定義の動作の2つの危険性の少ない兄弟、未指定の動作と実装定義の動作についても言及しています。
この国際標準の意味論的記述は、パラメーター化された非決定論的な抽象マシンを定義しています。
抽象マシンの特定の側面と操作は、この国際標準で実装定義として記述されています(たとえば、
sizeof(int)
)。これらは、抽象マシンのパラメーターを構成します。各実装には、これらの点での特性と動作を説明するドキュメントが含まれます。抽象マシンの特定の他の側面と操作は、この国際標準では指定されていません(たとえば、関数の引数の評価順序)。可能であれば、この国際標準は一連の許容可能な動作を定義します。これらは、抽象マシンの非決定的な側面を定義します。
他の特定の操作は、この国際標準では未定義として記述されています(たとえば、nullポインターの逆参照の影響)。[ 注:この国際規格は、未定義の動作を含むプログラムの動作に要件を課していません。— エンドノート ]
具体的には、セクション1.3.24は次のように述べています。
許容される未定義の動作は、予測できない結果で完全に状況を無視することから、翻訳またはプログラムの実行中に、環境に特徴的な文書化された方法(診断メッセージの発行ありまたはなし)での動作、翻訳または実行の終了(発行あり)までの範囲です。診断メッセージの)。
未定義の動作に遭遇しないようにするにはどうすればよいですか?基本的に、あなたは彼らが何について話しているかを知っている作家による良いC ++の本を読む必要があります。インターネットのチュートリアルをねじ込みます。ねじブルズシルト。
int f(){int a; return a;}
:の値はa
、関数呼び出し間で変わる可能性があります。
まあ、これは基本的には標準からの単純なコピー&ペーストです
3.4.1 1 実装で定義された動作の不特定の動作。各実装では、選択方法が文書化されています。
2例実装定義の動作の例は、符号付き整数が右にシフトされたときの上位ビットの伝搬です。
3.4.3 1 この国際標準が要件を課さない、移植不能またはエラーのあるプログラム構造またはエラーのあるデータの使用時の未定義の動作動作
2注可能な未定義の動作は、予測できない結果を伴う状況の完全な無視から、(診断メッセージの発行の有無にかかわらず)環境に特徴的な文書化された方法での変換またはプログラムの実行中の動作、変換または実行の終了(診断メッセージの発行)。
3例未定義の動作の例は、整数オーバーフローの動作です。
3.4.4 1 不特定の動作不特定の値の使用、またはこの国際標準が2つ以上の可能性を提供し、どのインスタンスでも選択されるものにそれ以上の要件を課さないその他の動作
2例不特定の動作の例は、関数への引数が評価される順序です。
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
コンパイラ未定義の動作たinvokeミサイルを発射していない機能を呼び出すすべての手段が、それはへの呼び出し作ることができると判断することができるlaunch_missiles()
無条件に。
たぶん、簡単な言い回しは、規格の厳密な定義よりも理解しやすいかもしれません。
実装定義の動作
言語は、データ型を持っていると言っています。コンパイラベンダーは、使用するサイズを指定し、何を行ったかのドキュメントを提供します。
未定義の動作
何か間違っています。たとえば、にint
収まらないに非常に大きな値があるとしchar
ます。どのようにその値を入れますchar
か?実際には方法はありません!何かが起こる可能性がありますが、最も賢明なことは、そのintの最初のバイトを取り出してに入れることですchar
。最初のバイトを割り当てるためにそれを行うのは間違っていますが、それは内部で起こることです。
詳細不明の動作
これら2つの機能のうち、最初に実行されるのはどの機能ですか。
void fun(int n, int m);
int fun1()
{
cout << "fun1";
return 1;
}
int fun2()
{
cout << "fun2";
return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?
言語は評価を指定しません、左から右または右から左!したがって、不特定の動作が原因で未定義の動作が発生する場合と発生しない場合がありますが、プログラムで不特定の動作が発生することはありません。
@eSKayあなたの質問はもっと明確にするために答えを編集する価値があると思います:)
fun(fun1(), fun2());
「実装が定義された」動作ではないのですか?結局のところ、コンパイラはどちらか一方のコースを選択する必要がありますか?
実装定義と未指定の違いは、コンパイラは最初のケースで動作を選択することになっているが、2番目のケースでは動作を選択する必要がないことです。たとえば、実装にはの定義が1つだけ必要ですsizeof(int)
。したがって、sizeof(int)
プログラムのある部分では4であり、他の部分では8であるとは言えません。コンパイラがOKと言うことができる未指定の動作とは異なり、私はこれらの引数を左から右に評価し、次の関数の引数は右から左に評価されます。同じプログラムで発生する可能性があるため、詳細不明と呼ばれますます。実際、未指定の動作の一部が指定されていれば、C ++はより簡単になったかもしれません。で、ここを見てみましょうそのための博士Stroustrup氏の答え:
コンパイラーにこの自由度を与え、「通常の左から右への評価」を要求することで生成できるものとの違いは、大きくなる可能性があると主張されています。私は確信はありませんが、無数のコンパイラが自由を利用していて、自由を情熱的に擁護している人がいると、変更は難しく、CおよびC ++の世界の隅々まで浸透するのに数十年かかる可能性があります。すべてのコンパイラが++ i + i ++などのコードに対して警告するわけではないことに失望しています。同様に、引数の評価順序は指定されていません。
IMOは、あまりにも多くの「もの」が未定義、未指定、実装定義などのままになっています。しかし、それは簡単に言うことができ、例を示すことさえできますが、修正するのは困難です。また、ほとんどの問題を回避して移植可能なコードを生成することはそれほど難しいことではないことにも注意してください。
fun(fun1(), fun2());
行動ではないの"implementation defined"
ですか?結局のところ、コンパイラはどちらか一方のコースを選択する必要がありますか?
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
私はこれがcan
起こることを理解しています。私たちが最近使用しているコンパイラーで本当にそうですか?
公式のC Rationale Documentから
未指定の動作、未定義の動作、実装定義の用語は、規格が完全に記述していない、または記述できない特性を持つプログラムを記述した結果を分類するために使用されます。この分類を採用する目的は、実装への特定の多様性を可能にすることであり、実装の品質を市場で積極的に発揮できるようにするとともに、標準への準拠のキャッシュを削除することなく、特定の一般的な拡張を許可することです。標準の付録Fは、これらの3つのカテゴリのいずれかに該当する動作をカタログ化しています。
不特定の動作は、プログラムの翻訳において実装者にある程度の自由度を与えます。この寛容度は、プログラムの翻訳に失敗した場合には及びません。
未定義の動作は、診断が難しい特定のプログラムエラーを検出しないように実装者にライセンスを与えます。また、準拠する可能性のある言語拡張の領域も識別します。実装者は、公式に定義されていない動作の定義を提供することにより、言語を拡張できます。
実装定義の動作により、実装者は適切なアプローチを自由に選択できますが、この選択をユーザーに説明する必要があります。実装定義として指定された動作は、一般に、ユーザーが実装定義に基づいて有意義なコーディングを決定できる動作です。実装者は、実装の定義をどの程度拡張する必要があるかを決定するときに、この基準に留意する必要があります。不特定の動作と同様に、実装定義の動作を含むソースの翻訳に失敗しただけでは、適切な応答にはなりません。
未定義の動作と未指定の動作の簡単な説明があります。
彼らの最終的な要約:
要約すると、ソフトウェアが移植可能である必要がない限り、不特定の動作は通常、心配する必要のないものです。逆に、未定義の動作は常に望ましくなく、発生することはありません。
歴史的に、実装定義の動作と未定義の動作はどちらも、標準の作成者が品質の実装を書いている人が判断を使用して、動作保証が存在する場合、その動作が意図されたアプリケーションフィールドのプログラムに役立つと判断する状況を表しています。意図されたターゲット。ハイエンドの数値処理コードのニーズは、低レベルのシステムコードのニーズとはかなり異なります。UBとIDBはどちらも、それらの異なるニーズを満たす柔軟性をコンパイラーに提供します。どちらのカテゴリも、実装が特定の目的に役立つ方法で、またはどのような目的でも役立つように動作することを義務付けていません。ただし、特定の目的に適していると主張する品質の実装は、そのような目的に適した方法で動作する必要があります標準がそれを要求するかどうか。
実装で定義された動作と未定義の動作の唯一の違いは、前者は実装が何も役に立たない可能性がある場合でも、実装で一貫した動作を定義して文書化する必要があることです。それらの間の境界線は、振る舞いを定義することが実装に一般的に役立つかどうか(コンパイラー作成者は、標準がそれらを要求するかどうかにかかわらず、実用的なときに役立つ振る舞いを定義する必要があります)ではなく、動作の定義に同時にコストがかかる実装があるかどうかですそして役に立たない。そのような実装が存在する可能性があるという判断は、いかなる形でも、形や形ではなく、他のプラットフォームで定義された動作をサポートすることの有用性に関する判断を意味します。
残念なことに、1990年代半ば以降、コンパイラの作成者は、動作要件の欠如を、動作保証が重要であるアプリケーション分野でも、実質的にコストがかからないシステムでも、コストに見合わないとの判断として解釈し始めました。UBを合理的な判断を行うための招待状として扱う代わりに、コンパイラー作成者は、UBをそうしない言い訳として扱い始めました。
たとえば、次のコードがあるとします。
int scaled_velocity(int v, unsigned char pow)
{
if (v > 250)
v = 250;
if (v < -250)
v = -250;
return v << pow;
}
2の補数の実装は、式が正であるか負v << pow
であるかv
に関係なく、2の補数シフトとして式を処理するために努力を費やす必要はありません。
ただし、今日の一部のコンパイラ作成者の間で推奨されている哲学v
は、プログラムが未定義の動作に関与する場合にのみ負になる可能性があるため、プログラムに負の範囲をクリップさせる理由はないことを示唆していますv
。負の値の左シフトは、重要なすべてのコンパイラでサポートされていましたが、既存のコードの多くはその動作に依存していますが、現代の哲学では、左シフトの負の値はUBであると標準が言っているという事実を解釈します。コンパイラの作成者はそれを自由に無視できることを意味します。
<<
負の数でUBであるという事実は厄介な小さな罠であり、それを思い出して嬉しいです!
i+j>k
加算がオーバーフローする場合でも、他の副作用がない場合、プログラマーが1と0のどちらを生成するかを気にしない場合、コンパイラーは、プログラマーがとしてコードを記述した場合には不可能な大規模な最適化を行うことができます(int)((unsigned)i+j) > k
。
C ++標準n3337 § 1.3.10 処理系定義の動作
実装に依存し、各実装が文書化する、整形式のプログラム構造と正しいデータの動作
C ++標準では、一部の構成に特定の動作を課さない場合がありますが、代わりに、特定の明確に定義された動作は、特定の実装(ライブラリのバージョン)によって選択および記述される必要があると述べています。したがって、Standardがこれを記述していない場合でも、ユーザーはプログラムがどのように動作するかを正確に知ることができます。
C ++標準n3337 § 1.3.24 未定義の動作
この国際標準が要件を課さない動作[注:この国際標準が動作の明示的な定義を省略した場合、またはプログラムが誤った構成体または誤ったデータを使用した場合、未定義の動作が予期される可能性があります。許容される未定義の動作は、予測できない結果で完全に状況を無視することから、環境に特徴的な文書化された方法(診断メッセージの発行の有無にかかわらず)で動作すること、翻訳または実行を終了すること(発行あり)までさまざまです。診断メッセージの)。エラーのあるプログラム構造の多くは、未定義の動作を引き起こしません。診断する必要があります。—エンドノート]
プログラムがC ++標準に従って定義されていないコンストラクトに遭遇すると、それはやりたいことが何でもできるようになります(私にメールを送ったり、メールを送ったり、コードを完全に無視したりできます)。
C ++標準n3337 § 1.3.25 、不特定の行動
動作に依存する、整形式プログラム構造と正しいデータの動作[注:実装は、どの動作が発生するかを文書化する必要はありません。通常、考えられる動作の範囲は、この国際標準で規定されています。—エンドノート]
C ++標準では、特定の動作を一部の構成に課すことはありませんが、代わりに、特定の実装(ライブラリのバージョン)によって、明確に定義された特定の動作を選択する必要がある(ボットは説明不要)と述べています。したがって、説明が提供されていない場合、プログラムがどのように動作するかをユーザーが正確に把握することは困難です。
実装定義-
実装者は希望する、十分に文書化されるべきである、標準は選択を与えるが、必ずコンパイルする
未指定-
実装定義と同じですが、文書化されていません
未定義-
何かが起こる可能性がありますので、注意してください。
uint32_t s;
を評価、1u<<s
ときs
33は多分0または多分2を収量を得、何でも他の奇抜をしないと予想することができています。ただし、新しいコンパイラでは、評価1u<<s
することにより、s
事前に32未満である必要があるため、式の前後のコードs
が32以上である場合にのみ関連があるとコンパイラが判断する場合があります。