サーチ…
前書き
未定義ビヘイビア(UB)とは何ですか? ISO C ++標準(§1.3.24、N4296)によれば、「この国際標準が要件を課さない動作」です。
これは、プログラムがUBに遭遇したときに、それが望むものを何でもすることが許されることを意味する。これはしばしばクラッシュを意味しますが、単に何もしない、 鬼の鼻を飛ばす 、または正しく動作するように見えるかもしれません!
言うまでもなく、UBを呼び出すコードを書くのは避けるべきです。
備考
プログラムに未定義のビヘイビアが含まれている場合、C ++標準ではビヘイビアに制約はありません。
- 開発者が意図したとおりに動作するように見えるかもしれませんが、クラッシュしたり、奇妙な結果を生じる可能性があります。
- 動作は、同じプログラムの実行間で異なる場合があります。
- 定義されていない動作を含む行の前に来る行を含め、プログラムのどの部分も誤動作する可能性があります。
- 実装では、未定義の動作の結果を文書化する必要はありません。
実装は 、標準に従って未定義の動作を生成する動作の結果を記録することがありますが、そのような動作に依存するプログラムは移植性がありません。
未定義の動作が存在する理由
直感的には、未定義の振る舞いは、例外ハンドラのようなエラーをうまく扱うことができないため、悪いことです。
しかし、いくつかの動作を定義しないままにしておくことは、実際には、C ++の "あなたが使用していないものを支払わない"という約束の不可欠な部分です。未定義の動作により、コンパイラは開発者が自分が何をしているかを知り、上の例で強調表示されている誤りをチェックするためのコードを導入しないと想定することができます。
定義されていない動作の検出と回避
いくつかのツールを使用して、開発中に未定義の動作を発見することができます。
- ほとんどのコンパイラでは、コンパイル時に未定義の動作が発生することを警告する警告フラグがあります。
- gccとclangの新しいバージョンには、実行時にパフォーマンスコストで未定義の動作をチェックする、いわゆる "Undefined Behavior Sanitizer"フラグ(
-fsanitize=undefined
)が含まれています。 -
lint
ようなツールは、より完全な未定義の動作分析を実行することがあります。
C ++ 14標準(ISO / IEC 14882:2014)セクション1.9(プログラム実行)から:
この国際標準の意味論的記述は、パラメータ化された非決定論的抽象機械を定義する。 [カット]
抽象マシンの特定の側面と操作は、この標準で実装定義 (例えば
sizeof(int)
)として記述されている。これらは抽象機械のパラメータを構成する 。各実装には、これらの点で特性と動作を記述したドキュメントが含まれていなければなりません。 [カット]抽象機械のある種の他の局面および操作は、この規格では不特定のものとして記述されている(例えば、割り振り関数がメモリ割り当てに失敗した場合に新しい初期化子での式の評価)。可能であれば、この国際規格は一連の許容される行動を定義する。これらは抽象機械の非決定論的側面を定義する。したがって、抽象機械のインスタンスは、所与のプログラムおよび所与の入力に対して複数の可能な実行を有することができる。
特定の他の操作は、この規格では定義されていないと記述されています (または、例えば、
const
オブジェクトを変更しようとする効果)。 [ 注 :この規格は、未定義の動作を含むプログラムの動作には何も要求しない。 - 終了ノート ]
ヌルポインタによる読み書き
int *ptr = nullptr;
*ptr = 1; // Undefined behavior
これは、nullポインタが有効なオブジェクトを指していないため、 *ptr
に書き込むオブジェクトがないため、 未定義の動作です。
これは最も頻繁にセグメンテーションフォルトを引き起こしますが、それは未定義で何かが起こります。
非voidの戻り値の型を持つ関数のreturn文がありません
戻り値の型がvoid
でない関数でreturn
文を省略すると、 未定義の動作になります。
int function() {
// Missing return statement
}
int main() {
function(); //Undefined Behavior
}
ほとんどの現代のコンパイラはこの種の未定義の動作のためにコンパイル時に警告を出します。
注:ルールに対する唯一の例外はmain
です。 main
return
文がない場合、コンパイラは自動的にreturn 0;
挿入しreturn 0;
あなたのために、それは安全に除外することができます。
文字列リテラルの変更
char *str = "hello world";
str[0] = 'H';
"hello world"
は文字列リテラルなので、それを変更すると未定義の動作が得られます。
上の例のstr
の初期化は、C ++ 03で正式に廃止されました(将来のバージョンの標準からの削除予定)。 2003年以前の多くのコンパイラは、これに関する警告を出すかもしれません(例えば、疑わしい変換)。 2003年以降、コンパイラは通常、廃止予定の変換について警告します。
上記の例は不正であり、C ++ 11以降ではコンパイラ診断となります。同様の例は、次のような型変換を明示的に許可することによって未定義の振る舞いを示すように構築することができます。
char *str = const_cast<char *>("hello world");
str[0] = 'H';
範囲外インデックスへのアクセス
配列(またはそのための標準ライブラリコンテナは、すべて生の配列を使用して実装されているため)の範囲外のインデックスにアクセスすることは、 未定義の動作です。
int array[] = {1, 2, 3, 4, 5};
array[5] = 0; // Undefined behavior
配列の最後(この場合はarray + 5
)を指すポインタを持つことは許されていますが、有効な要素ではないため、逆参照できません。
const int *end = array + 5; // Pointer to one past the last index
for (int *p = array; p != end; ++p)
// Do something with `p`
一般に、境界外ポインタを作成することはできません。ポインタは、配列内の要素を指していなければなりません。
ゼロによる整数除算
int x = 5 / 0; // Undefined behavior
0
による除算は数学的に定義されていないので、これは未定義の振る舞いであることが理にかなっています。
しかしながら:
float x = 5.0f / 0.0f; // x is +infinity
ほとんどの実装では、 NaN
(分子が0.0f
場合)、 infinity
(分子が正の場合)または-infinity
(分子が負の場合)を返すためにゼロによる浮動小数点除算を定義するIEEE-754が実装されています。
符号付き整数オーバーフロー
int x = INT_MAX + 1;
// x can be anything -> Undefined behavior
式の評価中に結果が数学的に定義されていない場合、またはその型の表現可能な値の範囲にない場合、その動作は未定義です。
(C ++ 11標準段落5/4)
これは、通常、再現可能で非衝突的な振る舞いをもたらすので、開発者が観察された振る舞いに大きく依存するように誘惑される可能性があるので、より厄介なものの1つです。
一方:
unsigned int x = UINT_MAX + 1;
// x is 0
以来、よく定義されています:
署名されていないと宣言された符号なし整数は、
2^n
法とする算術の法則に従わなければならない。ここで、n
は整数の特定の値の値表現のビット数である。
(C ++ 11標準パラグラフ3.9.1 / 4)
コンパイラが未定義の動作を利用して最適化することがある
signed int x ;
if(x > x + 1)
{
//do something
}
ここでは、符号付き整数のオーバーフローが定義されていないため、コンパイラは決して起こらないと想定することが自由であり、したがって、ifブロックを最適化することができます
初期化されていないローカル変数の使用
int a;
std::cout << a; // Undefined behavior!
これは初期化されていないためa
未定義の動作になります。
これは、値が「不確定」であるか、または「その前にそのメモリ位置にあった値」であることが、間違って主張されることがよくあります。しかし、上の例では、未定義の動作を与えるのはa
の値にアクセスする行為です。実際には、この場合は「ガベージ値」の印刷が一般的な症状ですが、これは未定義の動作の1つの可能な形式に過ぎません。
(特定のハードウェアサポートに依存しているので)実際にはそうは考えにくいが、上のコードサンプルをコンパイルするときにコンパイラはプログラマにも同じように電気を流すことができる。このようなコンパイラとハードウェアのサポートによって、未定義ビヘイビアに対するこのような応答は、未定義ビヘイビアの真の意味の平均的な(リビング)プログラマの理解を著しく高めます。
unsigned char
型のunsigned char
値を使用しても、値を次のように使用すると未定義の動作は発生しません。
- 三項条件演算子の第2または第3オペランド;
- ビルトインカンマ演算子の右オペランド。
-
unsigned char
への変換のオペランド。 - 左辺オペランドも
unsigned char
型の場合は代入演算子の右オペランド。 -
unsigned char
オブジェクトの初期化子。
または値が破棄された場合。そのような場合、indeterminate値は、適用可能であれば、式の結果に単純に伝播します。
static
変数は常にゼロで初期化されます(可能な場合)。
static int a;
std::cout << a; // Defined behavior, 'a' is 0
複数の同一ではない定義(1つの定義ルール)
テンプレートのクラス、列挙型、インライン関数、テンプレート、またはメンバが外部リンケージを持ち、複数の翻訳単位で定義されている場合、すべての定義が同一であるか、または1つの定義ルール(ODR)に従って動作が定義されていません。
foo.h
:
class Foo {
public:
double x;
private:
int y;
};
Foo get_foo();
foo.cpp
:
#include "foo.h"
Foo get_foo() { /* implementation */ }
main.cpp
:
// I want access to the private member, so I am going to replace Foo with my own type
class Foo {
public:
double x;
int y;
};
Foo get_foo(); // declare this function ourselves since we aren't including foo.h
int main() {
Foo foo = get_foo();
// do something with foo.y
}
上記のプログラムは、外部リンクを持つクラス::Foo
2つの定義を異なる翻訳単位で含んでいるため、未定義の動作を示しますが、2つの定義は同一ではありません。 同じ翻訳単位内のクラスの再定義とは異なり、この問題はコンパイラによって診断される必要はありません。
メモリ割り当てと割り当て解除のペアが正しくありません
オブジェクトは、 new
によって割り当てられていて配列でない場合にのみ、 delete
によって割り当てを解除できます。 delete
の引数がnew
によって返されなかった場合、または配列の場合、動作は未定義です。
オブジェクトは、 new
によって割り当てられ、配列である場合にのみ、 delete[]
によって割り当てを解除できます。 delete[]
引数がnew
によって返されなかったか、または配列でない場合、動作は未定義です。
free
への引数がmalloc
によって返されなかった場合、動作は未定義です。
int* p1 = new int;
delete p1; // correct
// delete[] p1; // undefined
// free(p1); // undefined
int* p2 = new int[10];
delete[] p2; // correct
// delete p2; // undefined
// free(p2); // undefined
int* p3 = static_cast<int*>(malloc(sizeof(int)));
free(p3); // correct
// delete p3; // undefined
// delete[] p3; // undefined
このような問題は完全に回避することにより回避することができmalloc
とfree
生の上に標準ライブラリスマートポインタを好む、C ++プログラムでnew
およびdelete
、および好むstd::vector
とstd::string
生の上にnew
とdelete[]
。
オブジェクトを間違った型としてアクセスする
ほとんどの場合、あるタイプのオブジェクトにアクセスするのは違うタイプです(cv-qualifierを無視して)。例:
float x = 42;
int y = reinterpret_cast<int&>(x);
結果は未定義の動作です。
この厳密なエイリアシング規則にはいくつかの例外があります。
- クラス型のオブジェクトは、実際のクラス型の基本クラスであるかのようにアクセスできます。
- どんな型でも
char
またはunsigned char
としてアクセスできますが、その逆は真ではありません。char配列はあたかも任意の型であるかのようにアクセスできません。 - 符号付き整数型は、対応する符号なし型としてアクセスでき、 その逆も可能です。
関連する規則は、非静的メンバ関数が実際に関数の定義クラスまたは派生クラスと同じ型を持たないオブジェクトに対して呼び出された場合、未定義の動作が発生するということです。関数がオブジェクトにアクセスしなくても、これは当てはまります。
struct Base {
};
struct Derived : Base {
void f() {}
};
struct Unrelated {};
Unrelated u;
Derived& r1 = reinterpret_cast<Derived&>(u); // ok
r1.f(); // UB
Base b;
Derived& r2 = reinterpret_cast<Derived&>(b); // ok
r2.f(); // UB
浮動小数点のオーバーフロー
浮動小数点型を生成する算術演算が、結果の型の表現可能な値の範囲にない値を生成する場合、その動作はC ++標準に従って未定義ですが、マシンが準拠している可能性のある他の標準によって定義され、 IEEE 754のようなものである。
float x = 1.0;
for (int i = 0; i < 10000; i++) {
x *= 10.0; // will probably overflow eventually; undefined behavior
}
コンストラクタまたはデストラクタから(純粋な)仮想メンバを呼び出す
メンバー関数は、抽象クラスのコンストラクタ(またはデストラクタ)から呼び出すことができます。そのようなコンストラクタ(またはデストラクタ)から作成(または破棄)されるオブジェクトに対して、直接的または間接的に仮想呼び出しを仮想呼び出し(10.3)することの効果は定義されていません。
より一般的には、Scott MeyersなどのC ++当局は、コンストラクタやdstructorsから仮想関数(非純粋なものでも)を呼び出すことは決してありません。
上記のリンクから変更された次の例を考えてみましょう。
class transaction
{
public:
transaction(){ log_it(); }
virtual void log_it() const = 0;
};
class sell_transaction : public transaction
{
public:
virtual void log_it() const { /* Do something */ }
};
sell_transaction
オブジェクトを作成するsell_transaction
ます。
sell_transaction s;
これは、 transaction
のコンストラクタを最初に呼び出すsell_transaction
のコンストラクタを暗黙的に呼び出します。しかし、 transaction
のコンストラクタが呼び出されるとき、オブジェクトはまだタイプsell_transaction
ではなく、タイプのtransaction
だけです。
したがって、 transaction::transaction()
log_it
へのlog_it
は、直感的なことではないように思われます。つまり、 sell_transaction::log_it
呼び出します。
この例のように
log_it
が純粋仮想の場合、その動作は未定義です。log_it
が非純粋仮想であれば、transaction::log_it
が呼び出されます。
仮想デストラクタを持たない基本クラスへのポインタを介して派生オブジェクトを削除する。
class base { };
class derived: public base { };
int main() {
base* p = new derived();
delete p; // The is undefined behavior!
}
section [expr.delete]§5.3.5/ 3では、静的型がvirtual
デストラクタを持たないオブジェクトに対してdelete
が呼び出された場合、
削除されるオブジェクトの静的型がその動的型と異なる場合、静的型は、削除されるオブジェクトの動的型の基本クラスでなければならず、静的型は仮想型破棄子を持つか、動作は未定義です。
これは、派生クラスが基本クラスにデータメンバーを追加したかどうかにかかわらず、ケースです。
ダングリングリファレンスへのアクセス
範囲外になったオブジェクトや他の方法で破棄されたオブジェクトへの参照にアクセスすることは不正です。このような参照は、もはや有効なオブジェクトを参照していないので、 ぶら下がっていると言われています。
#include <iostream>
int& getX() {
int x = 42;
return x;
}
int main() {
int& r = getX();
std::cout << r << "\n";
}
この例では、 getX
が返るときにローカル変数x
がスコープから外れます。 ( 生涯の拡張は、それが定義されているブロックのスコープを越えてローカル変数の存続期間を延ばすことはできないことに注意してください。)したがって、 r
は不安定な参照です。動作して印刷するように見えるかもしれないが、このプログラムでは、未定義の動作をしている42
いくつかのケースでは。
`std`または` posix`名前空間の拡張
標準(17.6.4.2.1 / 1)は、一般にstd
名前空間を拡張することを禁じています。
宣言または定義を名前空間stdまたは名前空間std内の名前空間に追加する場合、C ++プログラムの動作は特に指定しない限り、定義されていません。
同じことがposix
(17.6.4.2.2 / 1)にも当てはまりposix
:
宣言または定義を名前空間posixまたは名前空間posix内の名前空間に別途指定しない限り追加すると、C ++プログラムの動作は未定義です。
次の点を考慮してください。
#include <algorithm>
namespace std
{
int foo(){}
}
標準では、同じ定義を定義するalgorithm
(またはそれに含まれるヘッダの1つ)を禁じているわけではないので、このコードはOne Definition Ruleに違反します。
したがって、一般的に、これは禁止されています。ただし、 特定の例外は許可されています。おそらく最も有用なのは、ユーザー定義型の特殊化を追加できることです。したがって、たとえば、あなたのコードが
class foo
{
// Stuff
};
それでは、以下はうまくいきます
namespace std
{
template<>
struct hash<foo>
{
public:
size_t operator()(const foo &f) const;
};
}
浮動小数点型への変換中または浮動小数点型からの変換時
変換中に:
- 整数型から浮動小数点型への変換、
- 浮動小数点型から整数型への変換、または
- 浮動小数点タイプからより短い浮動小数点タイプまで、
ソース値が宛先タイプで表現できる値の範囲外の場合、結果は未定義の動作になります。例:
double x = 1e100;
int y = x; // int probably cannot hold numbers that large, so this is UB
ベースから派生した静的キャストが無効です
static_cast
を使用して派生クラスへのポインタ(参照参照)を基本クラスへのポインタ(参照参照)に変換するが、派生クラス型のオブジェクトをオペランドが参照(参照)しない場合、定義されていません。 派生変換のベースを参照してください。
不一致関数ポインタ型による関数呼び出し
関数ポインタを介して関数を呼び出すには、関数ポインタの型が関数の型と完全に一致していなければなりません。それ以外の場合、動作は未定義です。例:
int f();
void (*p)() = reinterpret_cast<void(*)()>(f);
p(); // undefined
constオブジェクトの変更
const
オブジェクトを変更しようとすると、未定義の動作になります。これが適用されるconst
変数のメンバーconst
オブジェクト、およびクラスのメンバーは、宣言const
。 (ただし、 mutable
のメンバーconst
オブジェクトがないconst
)。
そのような試みはconst_cast
を通してconst_cast
ことができます:
const int x = 123;
const_cast<int&>(x) = 456;
std::cout << x << '\n';
コンパイラは通常、 const int
オブジェクトの値をインライン化するため、このコードはコンパイルして123
を出力する可能性があります。コンパイラは、 const
オブジェクトの値を読み取り専用メモリに配置することもできるため、セグメンテーションフォルトが発生する可能性があります。いずれの場合でも、動作は未定義であり、プログラムは何かを行う可能性があります。
次のプログラムははるかに微妙なエラーを隠します:
#include <iostream>
class Foo* instance;
class Foo {
public:
int get_x() const { return m_x; }
void set_x(int x) { m_x = x; }
private:
Foo(int x, Foo*& this_ref): m_x(x) {
this_ref = this;
}
int m_x;
friend const Foo& getFoo();
};
const Foo& getFoo() {
static const Foo foo(123, instance);
return foo;
}
void do_evil(int x) {
instance->set_x(x);
}
int main() {
const Foo& foo = getFoo();
do_evil(456);
std::cout << foo.get_x() << '\n';
}
このコードでは、 getFoo
はconst Foo
型のシングルトンを作成し、メンバーm_x
は123
初期化されます。その後do_evil
呼び出され、値のfoo.m_x
明らかに間違っていた何456に変更されましたか?
その名前にもかかわらず、 do_evil
は特に悪いことは何もしません。それはFoo*
を通してセッターを呼び出すことだけです。しかし、 const_cast
が使用されていなくても、そのポインタはconst Foo
オブジェクトを指しています。このポインタは、 Foo
のコンストラクタによって取得されました。 const
オブジェクトはなりませんconst
初期化が完了するまで、のでthis
型を持つFoo*
、ないconst Foo*
コンストラクタ内、。
したがって、このプログラムには明らかに危険な構成要素がないにもかかわらず、未定義の動作が発生します。
メンバーへのポインタを介して存在しないメンバーにアクセスする
オブジェクトへのポインタを介してオブジェクトの非静的メンバにアクセスするとき、オブジェクトが実際にポインタによって示されるメンバを含まない場合、その動作は未定義です。 (メンバへのそのようなポインタはstatic_cast
で取得できます)。
struct Base { int x; };
struct Derived : Base { int y; };
int Derived::*pdy = &Derived::y;
int Base::*pby = static_cast<int Base::*>(pdy);
Base* b1 = new Derived;
b1->*pby = 42; // ok; sets y in Derived object to 42
Base* b2 = new Base;
b2->*pby = 42; // undefined; there is no y member in Base
メンバーへのポインタに対する派生から基本への変換が正しくありません
static_cast
を使用してTD::*
をTB::*
に変換すると、指されるメンバーは、基本クラスまたはB
派生クラスであるクラスに属している必要があります。それ以外の場合、動作は未定義です。 メンバーへのポインタのための導出変換を参照
無効なポインタ演算
次のポインタ演算を使用すると、未定義の動作が発生します。
結果がポインタ・オペランドと同じ配列オブジェクトに属さない場合は、整数の加算または減算。 (ここでは、最後の要素1はまだ配列に属していると見なされます)。
int a[10]; int* p1 = &a[5]; int* p2 = p1 + 4; // ok; p2 points to a[9] int* p3 = p1 + 5; // ok; p2 points to one past the end of a int* p4 = p1 + 6; // UB int* p5 = p1 - 5; // ok; p2 points to a[0] int* p6 = p1 - 6; // UB int* p7 = p3 - 5; // ok; p7 points to a[5]
両方が同じ配列オブジェクトに属していない場合、2つのポインタの減算。 (やはり、最後の要素1は配列に属すると見なされます。)例外は2つのヌルポインタを減算して0を生成することです。
int a[10]; int b[10]; int *p1 = &a[8], *p2 = &a[3]; int d1 = p1 - p2; // yields 5 int *p3 = p1 + 2; // ok; p3 points to one past the end of a int d2 = p3 - p2; // yields 7 int *p4 = &b[0]; int d3 = p4 - p1; // UB
結果が
std::ptrdiff_t
オーバーフローした場合の2つのポインタの減算。いずれかのオペランドのpointee型が、(cv-qualificationを無視して)指定されたオブジェクトの動的型と一致しないポインタ演算。標準によると、 "特に、配列に派生クラス型のオブジェクトが含まれている場合、ポインタの算術演算に基本クラスへのポインタを使用できません。"
struct Base { int x; }; struct Derived : Base { int y; }; Derived a[10]; Base* p1 = &a[1]; // ok Base* p2 = p1 + 1; // UB; p1 points to Derived Base* p3 = p1 - 1; // likewise Base* p4 = &a[2]; // ok auto p5 = p4 - p1; // UB; p4 and p1 point to Derived const Derived* p6 = &a[1]; const Derived* p7 = p6 + 1; // ok; cv-qualifiers don't matter
無効なポジション数のシフト
ビルトインシフト演算子の場合、右オペランドは負ではなく、昇格された左オペランドのビット幅よりも厳密に小さくなければなりません。それ以外の場合、動作は未定義です。
const int a = 42;
const int b = a << -1; // UB
const int c = a << 0; // ok
const int d = a << 32; // UB if int is 32 bits or less
const int e = a >> 32; // also UB if int is 32 bits or less
const signed char f = 'x';
const int g = f << 10; // ok even if signed char is 10 bits or less;
// int must be at least 16 bits
[noreturn]関数から戻る
標準からの例[dcl.attr.noreturn]:
[[ noreturn ]] void f() {
throw "error"; // OK
}
[[ noreturn ]] void q(int i) { // behavior is undefined if called with an argument <= 0
if (i > 0)
throw "positive";
}
既に破棄されているオブジェクトを破棄する
この例では、後で自動的に破棄されるオブジェクトに対してデストラクタが明示的に呼び出されます。
struct S {
~S() { std::cout << "destroying S\n"; }
};
int main() {
S s;
s.~S();
} // UB: s destroyed a second time here
同様の問題は、 std::unique_ptr<T>
が自動または静的な記憶期間を持つT
を指すようにされたときに発生します。
void f(std::unique_ptr<S> p);
int main() {
S s;
std::unique_ptr<S> p(&s);
f(std::move(p)); // s destroyed upon return from f
} // UB: s destroyed
オブジェクトを2回破棄するもう1つの方法は、2つのshared_ptr
所有権を互いに共有することなくオブジェクトを管理することです。
void f(std::shared_ptr<S> p1, std::shared_ptr<S> p2);
int main() {
S* p = new S;
// I want to pass the same object twice...
std::shared_ptr<S> sp1(p);
std::shared_ptr<S> sp2(p);
f(sp1, sp2);
} // UB: both sp1 and sp2 will destroy s separately
// NB: this is correct:
// std::shared_ptr<S> sp(p);
// f(sp, sp);
無限のテンプレート再帰
標準からの例[temp.inst] / 17:
template<class T> class X {
X<T>* p; // OK
X<T*> a; // implicit generation of X<T> requires
// the implicit instantiation of X<T*> which requires
// the implicit instantiation of X<T**> which ...
};