C ++プログラマーが知っておくべき一般的な未定義の動作は何ですか?
言う、のように:
a[i] = i++;
NULL
ポインターの逆参照memcpy
重複するバッファをコピーするために使用します。int64_t i = 1; i <<= 72
未定義)int i; i++; cout << i;
)volatile
するsig_atomic_t
long int
#if
式で定義されたトークンを動的に生成する関数パラメータが評価される順序は未規定の動作です。(これにより、プログラムがクラッシュしたり、爆発したり、ピザを注文したりすることはありません...未定義の動作とは異なります。)
唯一の要件は、関数を呼び出す前にすべてのパラメーターを完全に評価する必要があることです。
これ:
// 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);
どちらでもかまいません。それはコンパイラ次第です。副作用によっては、結果が重要になる場合があります。
コンパイラは、式の評価部分を自由に並べ替えることができます (意味が変更されていないと仮定します)。
元の質問から:
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.
未定義の動作に加えて、同様に厄介な実装定義の動作もあります。
未定義の動作は、プログラムが何かを実行したときに、その結果が標準で指定されていない場合に発生します。
実装定義の動作は、結果が標準によって定義されていないが、実装が文書化する必要があるプログラムによるアクションです。例は、スタック オーバーフローの質問からの「マルチバイト文字リテラル」です。 これをコンパイルできない C コンパイラはありますか? .
実装で定義された動作は、移植を開始するときにのみ噛み付きます (ただし、新しいバージョンのコンパイラへのアップグレードも移植です!)
const
を使用してネスを削除した後、定数に代入しconst_cast<>
ます。
const int i = 10;
int *p = const_cast<int*>( &i );
*p = 1234; //Undefined
私のお気に入りは「テンプレートのインスタンス化における無限再帰」です。コンパイル時に未定義の動作が発生するのはこれだけだと思うからです。
変数は、1 つの式で 1 回だけ更新できます (技術的には、シーケンス ポイント間で 1 回)。
int i =1;
i = ++i;
// Undefined. Assignment to 'i' twice in the same expression.
さまざまな環境制限の基本的な理解。完全なリストは、C 仕様のセクション 5.2.4.1 にあります。ここにいくつかあります。
実際、switch ステートメントの case ラベルが 1023 という制限に少し驚きました。生成されたコード/lex/パーサーがかなり簡単に超えていることが予測できます。
これらの制限を超えると、未定義の動作 (クラッシュ、セキュリティ上の欠陥など) が発生します。
そうですね、これが C 仕様によるものであることは知っていますが、C++ はこれらの基本的なサポートを共有しています。
C++ がサイズを保証する唯一の型はchar
. サイズは 1 です。他のすべてのタイプのサイズは、プラットフォームに依存します。
memcpy
重複するメモリ領域間でコピーするために使用します。例えば:
char a[256] = {};
memcpy(a, a, sizeof(a));
この動作は、C++03 標準に組み込まれている C 標準に従って定義されていません。
あらすじ
1/ #include void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
説明
2/ memcpy 関数は、s2 が指すオブジェクトから s1 が指すオブジェクトに n 文字をコピーします。重複するオブジェクト間でコピーが行われる場合、動作は未定義です。戻り値 3 memcpy 関数は s1 の値を返します。
あらすじ
1 #include void *memmove(void *s1, const void *s2, size_t n);
説明
2 memmove 関数は、s2 が指すオブジェクトから s1 が指すオブジェクトに n 文字をコピーします。コピーは、s2 が指すオブジェクトの n 文字が最初に、s1 および s2 が指すオブジェクトと重複しない n 文字の一時配列にコピーされ、次に一時配列の n 文字がコピーされるかのように行われます。 s1 が指すオブジェクト。戻り値
3 memmove 関数は s1 の値を返します。
異なるコンパイル単位の名前空間レベルのオブジェクトは、初期化順序が定義されていないため、初期化のために相互に依存するべきではありません。