web-dev-qa-db-ja.com

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

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

次のように言います:

a[i] = i++;
201
yesraaj

ポインタ

  • NULLポインターの逆参照
  • サイズゼロの「新しい」割り当てによって返されるポインターの逆参照
  • ライフタイムが終了したオブジェクトへのポインタを使用する(たとえば、スタックに割り当てられたオブジェクトや削除されたオブジェクト)
  • まだ完全に初期化されていないポインターの逆参照
  • 配列の境界外(上または下)に結果を生成するポインター演算を実行します。
  • 配列の末尾を超えた位置でポインターを逆参照します。
  • 互換性のない型のオブジェクトへのポインターの変換
  • 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式で定義済みトークンを動的に生成する

分類される

  • 静的ストレージ期間を持つプログラムの破棄中にexitを呼び出す
233

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

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


この:

// 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);

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

31
Martin York

コンパイラは、式の評価部分を自由に並べ替えることができます(意味は変わらないと仮定します)。

元の質問から:

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.
27
Martin York

const_cast<>を使用してconstnessを除去した後に定数に割り当てる:

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

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

5

undefined behaviourの他に、同様に厄介なimplementation-defined behaviourもあります。

未定義の動作は、プログラムがその結果が標準で指定されていない何かを行うときに発生します。

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

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

5
Constantin

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

int i =1;
i = ++i;

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

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

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

Switchステートメントの1023個のcaseラベルの制限に実際には少し驚いていました。生成されたコード/ Lex /パーサーの超過はかなり簡単に予測できます。

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

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

3

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

2
yesraaj

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

2
JaredPar

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が指すオブジェクトからs1が指すオブジェクトにn文字をコピーします。重複するオブジェクト間でコピーが行われる場合、動作は未定義です。戻り値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
John Dibling