未定義、未指定、実装定義の動作


530

CおよびC ++の未定義の動作とは何ですか?不特定の動作と実装定義の動作はどうですか?それらの違いは何ですか?


1
私たちはこれまでにこれを完了していると確信していましたが、見つかりません。参照:stackoverflow.com/questions/2301372/...
dmckee ---元司会者の子猫



1
ここに興味深い議論があります(「付録Lと未定義の動作」のセクション)。
オーウェン

回答:


406

未定義の動作は、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 ++の本を読む必要があります。インターネットのチュートリアルをねじ込みます。ねじブルズシルト。


6
マージの結果、この回答はC ++のみをカバーするのは奇妙な事実ですが、この質問のタグにはCが含まれています。Cには、「未定義の動作」という別の概念があります。特定のルール違反(制約違反)に対して未定義であること。
ヨハネスシャウブ-litb 2010年

8
@Benoit規格では未定義の動作、期間と記載されているため、未定義の動作です。一部のシステムでは、実際に文字列リテラルは読み取り専用のテキストセグメントに格納され、文字列リテラルを変更しようとするとプログラムがクラッシュします。他のシステムでは、文字列リテラルは実際に変更されたように見えます。この規格は、何をすべきかを義務付けていません。これが未定義の動作の意味です。
fredoverflow 2013年

5
@FredOverflow、優れたコンパイラーはなぜ未定義の動作を与えるコードをコンパイルできるのですか?この種のコードをコンパイルすると、どのようメリットがありますか?未定義の動作を行うコードをコンパイルしようとしたときに、すべての優れたコンパイラが大きな赤い警告サインを表示しなかったのはなぜですか?
Pacerier 2013

14
@Pacerierコンパイル時にチェックできないものがあります。たとえば、nullポインタが逆参照されないことを保証することは常に可能ではありませんが、これは未定義です。
Tim Seguine、2013

4
@Celeritas、未定義の動作非決定的である可能があります。たとえば、初期化されていないメモリの内容を事前に知ることはできません。int f(){int a; return a;}:の値はa、関数呼び出し間で変わる可能性があります。
マーク

97

まあ、これは基本的には標準からの単純なコピー&ペーストです

3.4.1 1 実装で定義された動作の不特定の動作。各実装では、選択方法が文書化されています。

2例実装定義の動作の例は、符号付き整数が右にシフトされたときの上位ビットの伝搬です。

3.4.3 1 この国際標準が要件を課さない、移植不能またはエラーのあるプログラム構造またはエラーのあるデータの使用時の未定義の動作動作

2注可能な未定義の動作は、予測できない結果を伴う状況の完全な無視から、(診断メッセージの発行の有無にかかわらず)環境に特徴的な文書化された方法での変換またはプログラムの実行中の動作、変換または実行の終了(診断メッセージの発行)。

3例未定義の動作の例は、整数オーバーフローの動作です。

3.4.4 1 不特定の動作不特定の値の使用、またはこの国際標準が2つ以上の可能性を提供し、どのインスタンスでも選択されるものにそれ以上の要件を課さないその他の動作

2例不特定の動作の例は、関数への引数が評価される順序です。


3
実装定義の動作と未指定の動作の違いは何ですか?
ゾロモン

26
@Zolomon:それが言うように:基本的に同じことですが、実装が定義されている場合、実装は正確に何が起こるかを(保証するために)文書化する必要がありますが、不特定の場合、実装は文書化する必要はありませんまたは何かを保証します。
AnT

1
@Zolomon:3.4.1と2.4.4の違いに反映されています。
sbi 2010年

8
@Celeritas:ハイパーモダンコンパイラはそれよりも優れています。与えられたint foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }コンパイラ未定義の動作たinvokeミサイルを発射していない機能を呼び出すすべての手段が、それはへの呼び出し作ることができると判断することができるlaunch_missiles()無条件に。
スーパーキャット2015

2
@northerner引用が述べているように、不特定の動作は通常、可能な動作の限られたセットに制限されています。場合によっては、これらのすべての可能性が特定のコンテキストで許容可能であると結論付けることさえあります。その場合、不特定の動作はまったく問題ではありません。未定義の動作は完全に無制限です(eb「プログラムがハードドライブをフォーマットすることを決定する場合があります」)。未定義の動作は常に問題です。
AnT

60

たぶん、簡単な言い回しは、規格の厳密な定義よりも理解しやすいかもしれません。

実装定義の動作
言語は、データ型を持っていると言っています。コンパイラベンダーは、使用するサイズを指定し、何を行ったかのドキュメントを提供します。

未定義の動作
何か間違っています。たとえば、に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は、あまりにも多くの「もの」が未定義、未指定、実装定義などのままになっています。しかし、それは簡単に言うことができ、例を示すことさえできますが、修正するのは困難です。また、ほとんどの問題を回避して移植可能なコードを生成することはそれほど難しいことではないことにも注意してください。


1
fun(fun1(), fun2());行動ではないの"implementation defined"ですか?結局のところ、コンパイラはどちらか一方のコースを選択する必要がありますか?
Lazer

1
@AraK:説明してくれてありがとう。私はそれを理解しました。ところで、"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"私はこれがcan起こることを理解しています。私たちが最近使用しているコンパイラーで本当にそうですか?
Lazer

1
@eSKay多くのコンパイラで手を汚したこのことについての第一人者に尋ねる必要があります:) AFAIK VCは引数を常に右から左に評価します。
AraK 2010年

4
@Lazer:それは間違いなく起こり得ます。単純なシナリオ:foo(bar、boz())およびfoo(boz()、bar)。ここで、barはintであり、boz()はintを返す関数です。パラメータがレジスタR0〜R1で渡されることが予想されるCPUを想定します。関数の結果はR0に返されます。関数がR1を破棄する可能性があります。「boz()」の前に「bar」を評価するには、boz()を呼び出して保存したコピーをロードする前に、他の場所にbarのコピーを保存する必要があります。「boz()」の後に「bar」を評価すると、メモリの保存と再フェッチが回避され、多くのコンパイラが引数リストでの順序に関係なく行う最適化になります。
スーパーキャット2011年

6
C ++については知りませんが、C標準では、intからcharへの変換は実装で定義されているか、または(実際の値と型の符号に応じて)明確に定義されているとしています。C99§6.3.1.3(C11で変更なし)を参照してください。
ニコライルーエ2013年

27

公式のC Rationale Documentから

未指定の動作、未定義の動作、実装定義の用語は、規格が完全に記述していない、または記述できない特性を持つプログラムを記述した結果を分類するために使用されます。この分類を採用する目的は、実装への特定の多様性を可能にすることであり、実装の品質を市場で積極的に発揮できるようにするとともに、標準への準拠のキャッシュを削除することなく、特定の一般的な拡張を許可することです。標準の付録Fは、これらの3つのカテゴリのいずれかに該当する動作をカタログ化しています。

不特定の動作は、プログラムの翻訳において実装者にある程度の自由度を与えます。この寛容度は、プログラムの翻訳に失敗した場合には及びません。

未定義の動作は、診断が難しい特定のプログラムエラーを検出しないように実装者にライセンスを与えます。また、準拠する可能性のある言語拡張の領域も識別します。実装者は、公式に定義されていない動作の定義を提供することにより、言語を拡張できます。

実装定義の動作により、実装者は適切なアプローチを自由に選択できますが、この選択をユーザーに説明する必要があります。実装定義として指定された動作は、一般に、ユーザーが実装定義に基づいて有意義なコーディングを決定できる動作です。実装者は、実装の定義をどの程度拡張する必要があるかを決定するときに、この基準に留意する必要があります。不特定の動作と同様に、実装定義の動作を含むソースの翻訳に失敗しただけでは、適切な応答にはなりません。


3
ハイパーモダンコンパイラライターは、「未定義の動作」を、プログラムが未定義の動作の原因となる入力を決して受け取らないと想定し、そのような入力を受け取ったときのプログラムの動作のすべての側面を任意に変更することをコンパイラライターに許可すると見なします。
スーパーキャット2016年

2
私が気付いた別のポイント:C89は、一部の実装では保証されているが他では保証されていない機能を説明するために「拡張」という用語を使用していません。C89の作成者は、当時のほとんどの実装では、結果が特定の方法で使用され、そのような処理が符号付きオーバーフローの場合にも適用される場合を除いて、符号付き算術と符号なし算術を同じように扱うことを認識していました。彼らはそれを附属書J2の一般的な拡張としてリストしませんでした、それは私に彼らがそれを拡張というよりむしろ自然な状態と見なしたことを示唆しています。
スーパーキャット

10

未定義の動作と未指定の動作の簡単な説明があります。

彼らの最終的な要約:

要約すると、ソフトウェアが移植可能である必要がない限り、不特定の動作は通常、心配する必要のないものです。逆に、未定義の動作は常に望ましくなく、発生することはありません。


1
コンパイラーには2種類あります。特に明記されていない限り、標準の未定義の動作のほとんどの形式を、基礎となる環境によって文書化された特性動作にフォールバックすると解釈するものと、デフォルトで、標準として特徴付けられている動作のみを有効に公開するものです。実装定義。最初のタイプのコンパイラを使用する場合、UBを使用すると、最初のタイプの多くのことを効率的かつ安全に実行できます。2番目のタイプのコンパイラは、そのような場合の動作を保証するオプションを提供する場合にのみ、そのようなタスクに適しています。
スーパーキャット2017

8

歴史的に、実装定義の動作と未定義の動作はどちらも、標準の作成者が品質の実装を書いている人が判断を使用して、動作保証が存在する場合、その動作が意図されたアプリケーションフィールドのプログラムに役立つと判断する状況を表しています。意図されたターゲット。ハイエンドの数値処理コードのニーズは、低レベルのシステムコードのニーズとはかなり異なります。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のいくつかのケースでそのような奇妙な動作を示す理由は、それらが執拗に最適化されていることであり、その点で最良の仕事をするには、UBが決して発生しないと想定できる必要があります。
トムスワーリー2016年

しかし、<<負の数でUBであるという事実は厄介な小さな罠であり、それを思い出して嬉しいです!
トム渦巻き模様の

1
@TomSwirly:残念ながら、コンパイラの作成者は、標準で規定されている以上の緩やかな動作保証を提供することで、標準で定義されていないコストをすべてコードで回避する必要がある場合と比較して、大幅な速度向上が可能になることを気にしません。i+j>k加算がオーバーフローする場合でも、他の副作用がない場合、プログラマーが1と0のどちらを生成するかを気にしない場合、コンパイラーは、プログラマーがとしてコードを記述した場合には不可能な大規模な最適化を行うことができます(int)((unsigned)i+j) > k
スーパーキャット2016年

1
@TomSwirly:彼らにとって、コンパイラーXが厳密に準拠するプログラムを使用してタスクTを実行し、コンパイラーYが同じプログラムで生成するよりも5%効率的な実行可能ファイルを生成できる場合、それはYであってもXの方が優れていることを意味します。 Yが保証するがXは保証しない動作を利用するプログラムを与えられた場合、同じタスクを3回効率的に実行するコードを生成できます。
スーパーキャット2016年

6

C ++標準n3337 § 1.3.10 処理系定義の動作

実装に依存し、各実装が文書化する、整形式のプログラム構造と正しいデータの動作

C ++標準では、一部の構成に特定の動作を課さない場合がありますが、代わりに、特定の明確に定義された動作は、特定の実装(ライブラリのバージョン)によって選択および記述される必要があると述べています。したがって、Standardがこれを記述していない場合でも、ユーザーはプログラムがどのように動作するかを正確に知ることができます。


C ++標準n3337 § 1.3.24 未定義の動作

この国際標準が要件を課さない動作[注:この国際標準が動作の明示的な定義を省略した場合、またはプログラムが誤った構成体または誤ったデータを使用した場合、未定義の動作が予期される可能性があります。許容される未定義の動作は、予測できない結果で完全に状況を無視することから、環境に特徴的な文書化された方法(診断メッセージの発行の有無にかかわらず)で動作すること、翻訳または実行を終了すること(発行あり)までさまざまです。診断メッセージの)。エラーのあるプログラム構造の多くは、未定義の動作を引き起こしません。診断する必要があります。—エンドノート]

プログラムがC ++標準に従って定義されていないコンストラクトに遭遇すると、それはやりたいことが何でもできるようになります(私にメールを送ったり、メールを送ったり、コードを完全に無視したりできます)。


C ++標準n3337 § 1.3.25 、不特定の行動

動作に依存する、整形式プログラム構造と正しいデータの動作[注:実装は、どの動作が発生するかを文書化する必要はありません。通常、考えられる動作の範囲は、この国際標準で規定されています。—エンドノート]

C ++標準では、特定の動作を一部の構成に課すことはありませんが、代わりに、特定の実装(ライブラリのバージョン)によって、明確に定義された特定の動作を選択する必要があるボットは説明不要)と述べています。したがって、説明が提供されていない場合、プログラムがどのように動作するかをユーザーが正確に把握することは困難です。


6

実装定義-

実装者は希望する、十分に文書化されるべきである、標準は選択を与えるが、必ずコンパイルする

未指定-

実装定義と同じですが、文書化されていません

未定義-

何かが起こる可能性がありますので、注意してください。


2
「未定義」の実用的な意味は、ここ数年で変わったことに注意することが重要だと思います。これは、与えられたことにするために使用uint32_t s;を評価、1u<<sときs33は多分0または多分2を収量を得、何でも他の奇抜をしないと予想することができています。ただし、新しいコンパイラでは、評価1u<<sすることにより、s事前に32未満である必要があるため、式の前後のコードsが32以上である場合にのみ関連があるとコンパイラが判断する場合があります。
スーパーキャット2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.