C ++プログラマが知っておくべき一般的な未定義の動作は何ですか?[閉まっている]


201

C ++プログラマが知っておくべき一般的な未定義の動作は何ですか?

次のように言います:

a[i] = i++;


3
本気ですか。それは明確に見えます。
マーティンヨーク

17
6.2.2 C ++プログラミング言語の評価順序[expr.evaluation]はそう言っています。他の参照はありません
yesraaj

4
彼は正しいです。C++プログラミング言語の6.2.2を見たところ、v [i] = i ++は定義されていません
dancavallaro

4
コンパイラがv [i]のメモリ位置を計算する前または後にi ++を実行するので、私は想像します。確かに、私は常にそこに割り当てられます。それは..操作の順序に応じ[I + 1] [I]又はVのいずれかのVに書くことができる
エヴァンテラン

2
C ++プログラミング言語が言うことは、「式内の部分式の演算の順序は定義されていません。特に、式が左から右に評価されると想定することはできません。」
dancavallaro 2008

回答:


233

ポインタ

  • NULLポインターの逆参照
  • サイズ0の「新しい」割り当てによって返されるポインターの逆参照
  • 存続期間が終了したオブジェクトへのポインターの使用(たとえば、スタックに割り当てられたオブジェクトまたは削除されたオブジェクト)
  • まだ明確に初期化されていないポインターの逆参照
  • 配列の境界の外(上または下)の外に結果を生成するポインター演算を実行します。
  • 配列の末尾を超えた位置でポインターを逆参照します。
  • 互換性のない型のオブジェクトへのポインターの変換
  • memcpy重複するバッファをコピーするために使用します

バッファオーバーフロー

  • 負のオフセット、またはそのオブジェクトのサイズを超えるオブジェクトまたは配列への読み取りまたは書き込み(スタック/ヒープオーバーフロー)

整数オーバーフロー

  • 符号付き整数オーバーフロー
  • 数学的に定義されていない式を評価する
  • 負の量の左シフト値(負の量の右シフトは実装定義)
  • 数値のビット数以上の量で値をシフトする(例:int64_t i = 1; i <<= 72未定義)

タイプ、キャスト、定数

  • 数値を、(直接またはstatic_castを介して)ターゲットタイプで表現できない値にキャストする
  • それは間違いなく割り当てられているの前に(例えば、自動変数を使用してint i; i++; cout << i;
  • シグナル以外の、volatileまたはsig_atomic_tシグナルの受信時のタイプのオブジェクトの値の使用
  • 有効期間中に文字列リテラルまたはその他のconstオブジェクトを変更しようとしています
  • 前処理中にナローとワイド文字列リテラルを連結する

機能とテンプレート

  • 値を返す関数から値を返さない(直接またはtryブロックから流出することによって)
  • 同じエンティティに対する複数の異なる定義(クラス、テンプレート、列挙、インライン関数、静的メンバー関数など)
  • テンプレートのインスタンス化における無限再帰
  • 異なるパラメーターを使用して関数を呼び出す、または関数へのリンクが定義されているパラメーターとリンケージへのリンケージ。

OOP

  • 静的な保存期間を持つオブジェクトのカスケード破棄
  • 部分的に重なっているオブジェクトに割り当てた結果
  • 静的オブジェクトの初期化中に関数を再帰的に再入力する
  • オブジェクトのコンストラクターまたはデストラクターからオブジェクトの純粋な仮想関数を呼び出す仮想関数
  • 構築されていないか、すでに破壊されているオブジェクトの非静的メンバーを参照する

ソースファイルと前処理

  • 改行で終わっていない、またはバックスラッシュで終わっている空でないソースファイル(C ++ 11より前)
  • 文字または文字列定数で指定されたエスケープコードの一部ではない文字が後に続くバックスラッシュ(これはC ++ 11で実装定義されています)。
  • 実装制限の超過(ネストされたブロックの数、プログラム内の関数の数、利用可能なスタックスペースなど)
  • で表現できないプリプロセッサ数値 long int
  • 関数のようなマクロ定義の左側にある前処理指令
  • #if式で定義されたトークンを動的に生成する

分類される

  • 静的ストレージ期間のあるプログラムの破棄中に出口を呼び出す

Hm ... NaN(x / 0)とInfinity(0/0)はIEE 754でカバーされていましたが、C ++が後で設計された場合、なぜx / 0を未定義として記録するのですか?
new123456

Re:「バックスラッシュの後に、文字または文字列定数で指定されたエスケープコードの一部ではない文字が続く。」これは、C89(§3.1.3.4)およびC ++ 03(C89を組み込んでいる)のUBですが、C99にはありません。C99は、「結果はトークンではなく、診断が必要である」と述べています(§6.4.4.4)。おそらくC ++ 0x(これにはC89が組み込まれています)も同じです。
アダム・ローゼンフィールド、2011年

1
C99標準には、付録J.2に未定義の動作のリストがあります。このリストをC ++に適合させるには、多少の作業が必要になります。C99句ではなく正しいC ++句への参照を変更し、無関係なものをすべて削除し、C ++だけでなくC ++でもこれらすべてが実際に定義されていないかどうかを確認する必要があります。
スティーブジェソップ

1
@ new123456-すべての浮動小数点ユニットがIEE754互換であるとは限りません。C ++がIEE754準拠を必要とする場合、コンパイラーは、明示的なチェックによってRHSがゼロであるケースをテストして処理する必要があります。動作を未定義にすることで、コンパイラーは、「非IEE754 FPUを使用すると、IEEE754 FPUの動作が得られない」と言って、そのオーバーヘッドを回避できます。
SecurityMatt

1
「結果が対応する型の範囲内にない式の評価」....整数オーバーフローは、符号なし整数型ではなく、符号なし整数型に対して明確に定義されています。
nacitar sevaht 2013

31

関数パラメーターが評価される順序は、不特定の動作です。(これにより、プログラムがクラッシュしたり、爆発したり、ピザを注文したりすることはありません未定義の動作とは異なります。)

唯一の要件は、関数を呼び出す前にすべてのパラメーターを完全に評価する必要があることです。


この:

// The simple obvious one.
callFunc(getA(),getB());

これと同等にすることができます:

int a = getA();
int b = getB();
callFunc(a,b);

またはこれ:

int b = getB();
int a = getA();
callFunc(a,b);

それはどちらかです。それはコンパイラ次第です。副作用によっては、結果が重要になる場合があります。


23
順序は指定されておらず、未定義ではありません。
Rob Kennedy

1
私はこれが嫌いです:)これらのケースの1つを追跡した後、1日の仕事を失いました...とにかく私のレッスンを学び、幸いにも再び落ちることはありませんでした
Robert Gould

2
@ロブ:私はここで意味の変化についてあなたと議論しますが、標準委員会がこれら2つの単語の正確な定義に非常にうるさいことを知っています。だから私はそれを変更します:-)
マーティンヨーク

2
これはラッキーだった。大学時代に噛まれて、教授に一目見てもらって、5秒くらいで問題を教えてもらいました。それ以外の場合にデバッグに無駄に費やす時間はわかりません。
トカゲに請求する

27

コンパイラーは、式の評価部分を自由に並べ替えることができます(意味が変更されていない場合)。

元の質問から:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

ダブルチェックロック。そして、1つの簡単な間違いを犯します。

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

シーケンスポイントとはどういう意味ですか?
yesraaj 2008


1
おお... Javaで推奨されているその正確な構造を見たので特に厄介です
Tom

一部のコンパイラは、この状況での動作を定義していることに注意してください。たとえば、VC ++ 2005+では、aが揮発性の場合、必要なメモリバリアがセットアップされ、命令の順序変更を防止して、ダブルチェックロックが機能するようにします。
Eclipse

マーティンヨーク:<i> //(c)は(a)と(b)の後に発生することが保証されています</ i> 確かにその特定の例で問題になる唯一のシナリオは、「i」がハードウェアレジスタにマップされた揮発性変数であり、a [i](「i」の古い値)がそれにエイリアスされている場合ですが、増分がシーケンスポイントの前に発生することを保証しますか?
スーパーキャット2010

5

私のお気に入りは「テンプレートのインスタンス化における無限再帰」です。これは、コンパイル時に未定義の動作が発生するのはこれだけだと私は信じているからです。


これは以前に行われましたが、どのように定義されていないのかわかりません。後から考えて無限の再帰を行うことは非常に明白です。
Robert Gould

問題は、コンパイラがコードを調べて、無限再帰の影響を受けるかどうかを正確に決定できないことです。これは停止問題の例です。参照:stackoverflow.com/questions/235984/...
ダニエルエリカー

ええ、それは間違いなく停止の問題です
ロバート・グールド

メモリが少なすぎるためにスワップが発生したため、システムがクラッシュしました。
Johannes Schaub-litb 2008

2
intに適合しないプリプロセッサ定数もコンパイル時間です。
ジョシュア

5

constを使用してネスを取り除いた後の定数への割り当てconst_cast<>

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

未定義の動作に加えて、同様に厄介な実装定義の動作もあります。

未定義の動作は、プログラムが標準で指定されていない結果を実行した場合に発生します。

実装定義の動作は、その結果が標準で定義されていないが実装が文書化する必要があるプログラムによるアクションです。例は、スタックオーバーフローの質問からの「マルチバイト文字リテラル」です。これをコンパイルできないCコンパイラはありますか?

実装で定義された動作は、移植を開始したときのみあなたを噛みます(ただし、新しいバージョンのコンパイラへのアップグレードも移植されます!)


4

変数は、式で1回だけ更新できます(技術的には、シーケンスポイント間で1回)。

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

2つのシーケンスポイント間で少なくとも 1回影響与えます。
Prasoon Saurav 2010

2
@Prasoon:あなたが意図したと思います:2つのシーケンスポイント間で最大 1回。:-)
Nawaz

3

さまざまな環境制限の基本的な理解。完全なリストは、C仕様のセクション5.2.4.1にあります。ここにいくつかあります。

  • 1つの関数定義に127個のパラメーター
  • 1回の関数呼び出しで127個の引数
  • 1つのマクロ定義に127個のパラメーター
  • 1回のマクロ呼び出しで127個の引数
  • 論理ソース行の4095文字
  • 文字列リテラルまたはワイド文字列リテラルの4095文字(連結後)
  • オブジェクト内の65535バイト(ホスト環境のみ)
  • 15 #includedfileのネストレベル
  • switchステートメントのケースラベル(ネストされたswitchステートメントのラベルを除く)

実際には、switchステートメントのケースラベルが1023の制限に少し驚いていましたが、生成されたコード/ lex /パーサーがかなり簡単に超えられることを予測できます。

これらの制限を超えると、動作が未定義になります(クラッシュ、セキュリティ上の欠陥など)。

そうです、これはC仕様によるものですが、C ++はこれらの基本的なサポートを共有しています。


9
これらの制限に達すると、未定義の動作よりも多くの問題が発生します。
new123456 2011

STD :: vectorなどのオブジェクトでは65535バイトを簡単に超えることができます
Demi

2

memcpy重複するメモリ領域間でコピーするために使用します。例えば:

char a[256] = {};
memcpy(a, a, sizeof(a));

この動作は、C ++ 03標準に含まれるC標準に従って定義されていません。

7.21.2.1 memcpy関数

あらすじ

1 / #include void * memcpy(void * restrict s1、const void * restrict s2、size_t n);

説明文

2 / memcpy関数は、s2が指すオブジェクトからn文字をs1が指すオブジェクトにコピーします。重複するオブジェクト間でコピーが行われた場合の動作は未定義です。戻り値3 memcpy関数はs1の値を返します。

7.21.2.2 memmove関数

あらすじ

1 #include void * memmove(void * s1、const void * s2、size_t n);

説明文

2 memmove関数は、s2が指すオブジェクトからn文字をs1が指すオブジェクトにコピーします。コピーは、s2が指すオブジェクトのn文字が、s1およびs2が指すオブジェクトと重ならないn文字の一時配列に最初にコピーされた後、一時配列のn文字がs1が指すオブジェクト。戻り値

3 memmove関数はs1の値を返します。


2

C ++がサイズを保証する唯一のタイプはcharです。そしてサイズは1です。他のすべてのタイプのサイズはプラットフォームに依存します。


それは<cstdint>の目的ではありませんか?uint16_6などのタイプを定義します。
Jasper Bekkers、2008

はい、しかしほとんどのタイプのサイズは、長いと言っても、明確に定義されていません。
JaredPar 2008

また、cstdintは現在のc ++標準にはまだ含まれていません。現在移植可能なソリューションについては、boost / stdint.hppを参照してください。
Evan Teran

これは未定義の動作ではありません。標準では、サイズを定義する標準ではなく、準拠するプラットフォームがサイズを定義するとしています。
Daniel Earwicker 2008

1
@JaredPar:スレッドがたくさんある複雑な投稿なので、ここにまとめまし。つまり、「5。-2147483647と+2147483647をバイナリで表すには、32ビットが必要です。」
John Dibling 2012年

2

異なるコンパイル単位の名前空間レベルのオブジェクトは、初期化の順序が定義されていないため、相互に依存して初期化することはできません。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.