目次
2.4 Try ... catch がスローする例外オブジェクト処理
2.7 try ブロックと処理ブロックでのジャンプ構文の使用は固く禁じられています
2.11 カスタム例外は、標準の例外クラスから継承するのが最適です
4.2 マクロアサートは NDEBUG マクロと組み合わせて使用されます
4.4 コンパイル アサーション - static_assert
1. 例外の簡単な説明
1.1 例外とは
異常とは、一言で言えば、プログラムの正常な機能の外に存在するプログラムエラーであり、プログラムで例外が発生しても、例外がキャッチされない場合、プログラムは終了します。
C/C++ はメモリ管理に多くの自由と柔軟性を提供しますが、この言語で開発されたアプリケーションのセキュリティは、プログラマのテストと検出のリンクに大きく依存します。クラスの設計者は、カスタム クラス型を設計するときに、例外を制御する機能に特に注意を払う必要があります。
もちろん、クラス設計に関しては、C/C++ によってしばしば批判されるメモリ リークに加えて、論理例外などの他の例外キャプチャも注意して処理する必要があります: 範囲外の例外、無効なパラメーター、長さの例外など、および実行時例外: システム例外、フォーマット例外、範囲例外など。
コードを書いているとき、無意識のうちに異常なコードを書いてしまうことがよくあります。たとえば、ハンドルされていないポインタや生のポインタを使用したり、リソース リークが発生したり、準拠していないコンストラクタやデストラクタを書いたり、長時間実行していたプログラムが突然クラッシュしたりします。エグゼキュータとライブラリ ルーチンのサイズが大きくなり、実行速度が低下しました。
例外が発生した場合は、処理する必要があります。処理しないと、プログラムが異常終了します。C/C++ 例外処理は、プログラムが制御フローと情報を実行ポイントから、実行によって以前にトラバースされたポイントに関連付けられた処理コードに転送できる手段を提供します (つまり、例外処理は制御を呼び出しスタック Transfer に移動します。は「スタックの巻き戻し」であり、これについては後の章で詳しく説明します)。C/C++ は、throw 式、dynamic_cast、typeid、new 式、割り当て関数、および特定の例外をスローして特定のエラー状態 (std::vector :: など) を示すように設計された標準ライブラリ関数など、さまざまな例外処理メソッドを提供します。 at、std::string::substr など) はすべて例外をスローできます。
例外をキャッチするには、throw 式が try ブロック内または try ブロック内で呼び出される関数内にある必要があり、例外オブジェクトの型と一致する catch 句が必要です。
関数 (非メンバー関数、メンバー関数) を宣言する場合、次の仕様を指定して、関数がスローできる例外の種類を制限できます。
- 動的な例外の説明 (C++17 より前) (つまり、throw (型識別リスト (オプション))、次の章を参照)
- ◦ noexcept の説明 (C++11 から) (詳細については、このコラム (パート 7) のブログ投稿を参照してください。この記事では展開しません)。noexcept は、C++11 で廃止された throw() の改良版です。
例外処理中に発生するエラーは、std::terminate および std::unexpected (C++17 より前) によって処理されます ( 2 つのキーワードについては後の章で説明します)。
1.2 例外処理の概念
[1] エラーハンドリング
例外のスローは、関数からエラーを通知するために使用されます。「エラー」は通常、次の場合に限定されます。
- 有効な戻り値オブジェクトを生成できないなど、事後条件を満たさない。
- 呼び出さなければならない別の関数の前提条件が満たされません。
- (非プライベート メンバー関数の場合) クラスの不変条件を確立できません (もはや確立できません)。
つまり、コンストラクターとほとんどのオペレーターは、例外をスローしてプログラム エラーを報告する必要があります。さらに、いわゆるワイド コントラクト関数は、例外を使用して不正な入力を示します。たとえば、std::string::at には前提条件がありませんが、添え字が範囲外であることを示すために例外をスローします。
【2】セキュリティ異常
関数がエラー状態を報告した後、プログラムの状態を保護するために追加の保証を提供できます。以下は、広く認識されている 4 つの例外保証レベルであり、それぞれが他の厳密なスーパーセットです。
- 非スロー (nothrow) (または失敗しない) 例外の保証 --- 関数が例外をスローすることはありません。スタックのアンワインド中に呼び出される可能性のあるデストラクタやその他の関数は、スローしないことが期待されます (それ以外の場合は、エラーを報告または抑制します)。デストラクタのデフォルトは noexcept です。(C++11以上) スワップ関数、移動コンストラクタ、および強力な例外保証を提供するために使用されるその他の関数は、決して失敗しないことが期待されます (関数は常に成功します)。
- 強力な例外保証 --- 関数が例外をスローした場合、プログラムの状態は、関数が呼び出される前の状態に正確にロールバックされます。(例: std::vector::push_back)
- 基本的な例外の保証 --- 関数が例外をスローした場合、プログラムは何らかの有効な状態にあります。リソースがリークされることはなく、すべてのオブジェクトの不変条件はそのまま残ります。
- 例外保証なし - 関数が例外をスローした場合、プログラムは有効な状態にない可能性があります。リソース リーク、メモリ破損、または不変式を破壊するその他のエラーが発生した可能性があります。
さらに、ジェネリック コンポーネントは例外ニュートラル (例外ニュートラル) の保証も提供できます。テンプレート パラメーター (std::sort の Compare 関数オブジェクト、または std::make_shared の T のコンストラクターなど) 例外からスローされた場合、それは変更されずに呼び出し元に伝播されます。
[3]例外オブジェクト
任意の完全な型と void への cv ポインターは例外オブジェクトとしてスローできますが、すべての標準ライブラリ関数は値によって匿名の一時オブジェクトをスローし、これらのオブジェクトの型は std::exception から (直接的または間接的に) 派生します。ユーザー定義の例外は、通常、このパターンに従います。例外オブジェクトの不必要なコピーとオブジェクトのスライスを回避するために、catch 句を使用して参照によってキャプチャすることをお勧めします。
2. 例外処理
2.1 try...catch 例外処理構文
例外は無視できません。必要に応じて、c/c++ 標準操作により、開発者は try...catch を呼び出して例外をキャッチし、例外情報をオブジェクトに渡し、オブジェクトをスローできます。このオブジェクトを通じて、どのようなエラーがどのように発生したかを判断できます。例外を処理します。
try...catch は、1 つ以上の例外処理ブロック (catch 句) を複合ステートメントに関連付けます。その構文は次のように実装されます。
try 复合语句 处理块序列
//其中处理块序列是一或多个处理块的序列,它有下列语法:
catch (属性(可选) 类型说明符序列声明符) 复合语句
// (1) 声明一个具名形参的catch子句:try { /* */ } catch (const std::exception& e) { /* */ }
catch (属性(可选) 类型说明符序列抽象声明符(可选) ) 复合语句
//(2)声明一个无名形参的catch子句:try { /* */ } catch (const std::exception&) { /* */ }
catch ( ... ) 复合语句
//(3)catch-all处理块,可被任何异常激活:try { /* */ } catch (...) { /* */ }
/*参数说明
*复合语句 - 花括号环绕的语句序列
*属性 - (C++11 起) 任意数量的属性,应用于形参
*类型说明符序列 - 形参声明的一部分,与在函数形参列表中相同
*声明符 - 形参声明的一部分,与在函数形参列表中相同
*抽象声明符 - 无名形参声明的一部分,与在函数形参列表中相同
*/
try ブロックはキーワード try で始まり、その後に中括弧で囲まれたステートメント シーケンスのブロックが続きます。try ブロックの後には、1 つ以上の catch 句が続きます。各 catch 句は 3 つの部分で構成されます: キーワード catch、括弧で囲まれた単一の型または単一のオブジェクトの宣言 --- 例外指定子として知られる、および通常は中括弧で囲まれたステートメント ブロックです。例外を処理するために catch 句が選択されている場合は、関連するブロック ステートメントが実行されます。catch 句の実行が終了すると、プログラム フローは、
最後の catch 句の直後のステートメントからすぐに続行されます。
catch 句の仮パラメータ (type-specifier-sequence と declarator、または type-specifier-sequence と abstract-declarator) は、この catch 句へのエントリを引き起こす例外のタイプを決定します。右辺値参照型、抽象クラス、不完全型、または不完全型へのポインターにすることはできません(ただし、(おそらく cv 修飾された) void へのポインターは許可されます。仮パラメータの型が配列型または関数型の場合、対応するポインタ型として扱われます (関数宣言と同様)。
2.2 動的例外記述スロー
スロー例外指定子は、関数が直接的または間接的にスローする可能性がある例外をリストします。
/*显式动态异常说明
*类型标识列表 - 逗号分隔的类型标识列表,后附省略号(...)的类型标识表示包展开 (C++11 起)
*/
throw(类型标识列表(可选)) //(C++11 中弃用)(C++17 中移除)
この宣言は、関数型、関数ポインター型、関数参照型、メンバー関数ポインター型関数、変数、非静的データ メンバー宣言子、最上位関数宣言子、および仮パラメーター宣言子としてのみ使用できます。または、戻り型宣言子で発生します。 .
void f() throw(int); // OK:函数声明
void (*pf)() throw (int); // OK:函数指针声明
void g(void pfa() throw(int)); // OK:函数指针形参声明
typedef int (*pf)() throw(int); // 错误:typedef 声明
通常、try ブロック関数の本体で throw 例外指定子を直接呼び出して、例外オブジェクトをスローすることもあります。
void f() throw(int); // 函数声明,定义异常抛出为int型
void func() {
try {
f(); //隐式,间接
} catch (int& obj) {
std::cout << " the exception info: " << obj << '\n';
}
}
void func() {
try {
throw std::string("test"); //主动抛出异常对象,显式
} catch (std::string& obj) {
std::cout << " the exception info: " << obj << '\n';
}
}
スロー例外指定子の詳細については、次の章を参照してください。
2.3 標準特例制度
C++ では、任意のタイプの例外をスローできます。一般に、std::exception から派生したタイプをスローすることをお勧めします。std::exception は標準ライブラリによって提供される例外基底クラスです. これは, 例外クラスセットに一貫したインタフェースを提供します.標準ライブラリによって生成されるすべての例外は std::exception から継承されます. 標準例外クラス全体の継承システムは下の図に示されています。
これらの例外クラスのほとんどは名前でよく知られており, 基本クラス std::exception の定義も非常に単純です. カスタマイズする最も基本的なクラスメンバーと同様に, 簡単なメンバー関数 (構築, 仮想破壊, コピー代入と、例外の説明情報を返す関数):
//成员函数
(构造函数) 构造异常对象 (公开成员函数)
(析构函数) [虚] 析构该异常对象 (虚公开成员函数)
operator= 复制异常对象 (公开成员函数)
what [虚] 返回解释性字符串 (虚公开成员函数)
//类声明
namespace std {
class exception {
public:
exception() noexcept;
exception(const exception&) noexcept;
exception& operator=(const exception&) noexcept;
virtual ~exception();
virtual const char* what() const noexcept;
};
}
2.4 Try ... catch がスローする例外オブジェクト処理
C++ 例外は、スローされた例外と同じ型の catch 式、または任意の型の例外をキャッチできる catch(...) 式によってキャッチできます。スローされた例外の型が、基底クラスまたは複数のサブクラスを持つクラスである場合、この型またはこの型の基底クラスへの参照を受け取ると、例外をキャッチできます。例外が参照によってキャッチされると、スローされた実際の例外オブジェクトにバインドされることに注意してください。
#include <iostream>
#include <vector>
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
//
try {
std::cout << "Creating a vector of size 5... \n";
std::vector<int> v(5);
std::cout << "Accessing the 11th element of the vector...\n";
std::cout << v.at(10); // vector::at() 抛出 std::out_of_range
} catch (const std::exception& e) { // 按基类的引用捕获
std::cout << " a standard exception was caught, with message '"
<< e.what() << "'\n";
}
}
オブジェクトをスローすると、例外がスローされます。このオブジェクトのタイプによって、アクティブにする処理コードが決まります。選択されたハンドラーは、オブジェクト タイプに一致し、例外がスローされた場所に最も近いコール チェーン内のハンドラーです。try 複合ステートメント内のいずれかのステートメントがタイプ E の例外をスローすると、それは、catch 節がリストされている順序で、処理ブロック シーケンス内の各 catch 節の仮パラメーター タイプ T と照合されます。次のいずれかに該当する場合、例外が一致します。
- E は T と同じ型です (T のトップレベルの cv 修飾子は無視されます)
- T は (cv 修飾された) E への左辺値参照です
- T は、E の明確なパブリック基本クラスです。
- T は、E の明確なパブリック基本クラスへの参照です。
- T は (cv 修飾された) U または const U& (C++14 以上) であり、U はポインターまたはメンバーへのポインター (C++17 以上) 型であり、E は次の 1 つ以上を渡すこともできます。ポインタ変換、修飾変換、関数ポインタ変換 (C++17以上) を非公開、保護、またはあいまいな基底クラスへのポインタ以外の U に変換するメンバ型 (C++17以上) へのポインタまたはポインタの変換
- T はポインタまたはメンバへのポインタ、または const ポインタへの参照 (C++14以上) であり、E は std::nullptr_t です。
try {
throw E;
} catch (const std::overflow_error& e) {
// 若 E 抛出 std::overflow_error 则执行之(“相同类型”规则)
} catch (const std::runtime_error& e) {
// 若 E 抛出 std::underflow_error 则执行之(“基类”规则)
} catch (const std::exception& e) {
// 若 E抛出 std::logic_error 则执行之(“基类”规则)
} catch (...) {
// 若 E 抛出 std::string 或 int 或任何其他无关类型则执行之
}
特に、catch-all 句 catch (...) はあらゆるタイプの例外に一致することに注意してください。存在する場合、それ(catch-all) は一連の処理ブロックの最後の catch 句でなければなりません。キャッチオール ブロックを使用すると、スローされないことを保証する関数から、キャッチされない例外がエスケープされないようにすることができます。
catch (...) {
//our code
}
すべての例外をキャッチする catch 句は、あらゆる種類の例外に一致します。catch(...) を他の catch 句と組み合わせて使用する場合は、最後にする必要があります。そうしないと、後続の catch 句は一致しません。
例外を処理するために catch が例外のタイプを知るだけでよい場合、例外指定子は仮パラメータ名を省略できます; 処理コードが発生した例外のタイプ以外の情報を必要とする場合、例外指定子は仮パラメータを含みます名前で、catch はこれを使用して例外オブジェクトに名前でアクセスします。
void g1()
{
try {
//...code
} catch (const std::exception& ) {
// 仅需要匹配发生了那种类型异常,异常处理与对象信息无关要紧
} catch (...) {
// 强制处理异常,与对象类型及信息无关要紧!
}
}
2.5 例外捕捉処理レベル
すべての catch 句をテストした後に一致が見つからない場合、throw 式で説明されているように、例外の伝播は周囲の try ブロックに続行されます。囲んでいる try ブロックが残っていない場合、std::terminate が実行されます (この場合、スタックの巻き戻しがまったく行われるかどうかは実装定義です。キャッチされない例外をスローすると、デストラクタを呼び出さずにプログラムを終了させることができます)。 .
void f()
{
try{
std::invalid_argument e("test");
throw e; //主动抛出一个异常
}catch (const std::exception& e){
//类型不匹配被跳过
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {
// ...
} catch (const std::runtime_error& e) {
// ...
} catch (const std::exception& e) {
// ...
} catch (...) {
// ...,这里捕获
}
}
基本クラスの例外指定子を使用して、派生型の例外オブジェクトをキャッチできます。例外指定子の静的型によって、catch 句が実行できるアクションが決まります。スローされた例外オブジェクトが派生クラス型であるが、基本クラス型を受け入れる catch によって処理される場合、catch は派生クラスに固有のメンバーを使用してはなりません。
派生クラスの catch 句が基本クラスの catch 句の後に配置されている場合、派生クラスの catch 句は実行されません。例外の種類をクラス階層に編成する場合、設計者は、アプリケーションによって例外が処理される粒度のレベルを慎重に選択する必要があります。
void g1()
{
try {
//...code
} catch (const std::exception& e) {
// 若 f() 抛 std::runtime_error 则执行
} catch (const std::runtime_error& e) {//warning: exception of type 'std::runtime_error' will be caught by earlier handler [-Wexceptions]
// 死代码!
}
}
2.6 オブジェクトメソッドのスロー
継承によって関連付けられた型の例外を catch 句で処理する場合は、その仮パラメーターを参照として定義する必要があります。
catch パラメーターが参照型の場合、catch オブジェクトは例外オブジェクトに直接アクセスし、catch オブジェクトの静的型は、catch オブジェクトによって参照される例外オブジェクトの動的型とは異なる場合があります。
例外指定子が参照でない場合、catch オブジェクトは例外オブジェクトのコピーであり、catch オブジェクトが基本クラス型のオブジェクトであり、例外オブジェクトが派生型の場合、例外オブジェクトはそのオブジェクトに分割されます。基本クラスのサブオブジェクト。オブジェクト (参照とは対照的に) は、その時点では多態的ではありません。参照ではなくオブジェクトを介して仮想関数を使用する場合、オブジェクトの静的型は動的型と同じであり、関数が仮想である場合も同じです。動的バインディングは、参照またはポインターによって呼び出された場合にのみ発生し、オブジェクトによって呼び出された場合は動的バインディングは発生しません。
catch 句を入力すると、その仮パラメータが例外タイプの基本クラスである場合、例外オブジェクトの基本クラス サブオブジェクトからコピー初期化されます。それ以外の場合は、例外オブジェクトからコピー初期化されます (このコピーはコピー除外規則に従います)。catch 句の仮引数が参照型の場合、変更内容は例外オブジェクトに反映され、例外が throw; で再スローされた場合、別の処理ブロックで監視できます。パラメータが参照でない場合、パラメータへの変更はローカルであり、その有効期間は処理ブロックが終了すると終了します。
#include <string>
void g2()
{
try {
std::string("abc").substr(10); // 抛出 std::length_error
// } catch (std::exception e) { // 从 std::exception 基类复制初始化
// std::cout << e.what(); // 丢失来自 length_error 的信息
// }
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what(); // 打印来自 length_error 的信息
}
}
C++11 以降では、catch 句で std::current_exception を使用して例外を std::exception_ptr にキャプチャでき、std::throw_with_nested を使用してネストされた例外を構築できます。
2.7 try ブロックと処理ブロックでのジャンプ構文の使用は固く禁じられています
goto ステートメントと switch ステートメントを使用して、制御を try ブロックと処理ブロックに移すことはできません。goto を使用して try ブロックを終了する場合、およびこの goto によって実行されるブロック スコープの自動変数のデストラクタが例外をスローして終了する場合、それらの例外は、変数が定義されている try ブロックによってキャッチされます。
class A1{};
class A2{};
void g3()
{
int i=0;
label:
try {
A1 a1;
try {
A2 a2;
if(i<2)
goto label; // 销毁 a2,然后销毁 a1,再跳到 label
} catch (...) { } // 捕捉来自 a2 析构函数的异常
} catch (...) { } // 捕捉来自 a1 析构函数的异常
}
例外のスローまたは再スローに加えて、通常の try ブロック (関数の try ブロックではない) に続く catch 句は、return、continue、break、goto によって、またはその複合ステートメントの最後に到達することによって終了できます。これらのいずれの場合でも、例外オブジェクトは破棄されます (それを参照する std::exception_ptr のインスタンスがない限り)。
2.8 例外捕捉と例外処理の分離
C++ の try ... catch 例外処理では、問題検出部分が処理コードにオブジェクトを投げる必要があり、オブジェクトの型と内容から例外情報を判別して処理します。例外を通じて、問題の検出と問題の解決を分離できます. プログラムの一部は、この部分では解決できない問題を検出できます. この問題検出部分は、問題を処理する準備ができている他の部分に問題を渡すことができます.プログラムの問題検出部分 問題の処理方法を知る必要はありません。
class exception_def: public std::exception{}; //自定义异常类
void f(const exception_def& e)
{
//code,专门处理异常的函数
}
void g5() {
try {
exception_def e;
throw e; //传递一个自定义对象
} catch (const exception_def& e) { // 多态对象派生类的引用
f(e);
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
tryブロックでは、スローが実行されると、スローされたオブジェクトに続くステートメントは実行されませんが、制御はスローされたオブジェクトの実行ステートメントから、同じ関数内のローカルキャッチである可能性のある一致するキャッチに転送されます。または、例外が発生した関数を直接的または間接的に呼び出す別の関数内。
2.9 配列と関数型の転送
c/c++ コンパイルでは、配列または関数型の引数が渡されると、引数は自動的にポインターに変換されます。スローされたオブジェクトに対しても同じ自動変換が行われるため、配列または関数型の例外はありません。逆に、配列がスローされた場合、スローされたオブジェクトは配列の最初の要素へのポインターに変換されます。
void g6() {
try {
int excep[2]={1,2};
throw excep; //转换为指针
} catch (int e[]) { // 转换为指针
//
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
同様に、関数がスローされた場合、関数オブジェクトを直接作成して渡すのではなく、関数は関数へのポインターに変換されます。
class OperObj
{
public:
void operator()(){};
};
void g6() {
try {
OperObj e;
throw e; //转换为指针
} catch (OperObj& e) { // 转换为指针
e(); //
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
2.10 例外とポインタ
スロー式でポインターをスローすると、常にスロー内のポインターの逆参照に問題が発生します。ポインターを逆参照した結果は、ポインターの型と一致する型を持つオブジェクトになります。ポインターが継承階層内の型を指している場合、ポインターが指すオブジェクトの型は、ポインターの型とは異なる場合があります。オブジェクトの実際の型に関係なく、例外オブジェクトの型はポインターの静的型と一致します。ポインターが、派生クラス オブジェクトへの基本クラス型ポインターへのポインターである場合、そのオブジェクトは分割され、基本クラスの部分のみがスローされます。
class exception_PTR: public std::exception{};
void g8() {
std::exception *eptr= new exception_PTR();
try {
//do something
throw eptr; //抛出局部指针
} catch (const std::exception* e) { // 多态对象基类的引用
//只能使用基类的成员函数,派生类内重载的不可用
std::cout << e->what();
}
if(nullptr!=eptr){
delete eptr;
eptr = nullptr;
}
}
関数からローカル オブジェクトへのポインターを返すことが間違っているのと同じ理由で、ポインター 自体がスローされる場合、ローカル オブジェクトへのポインターをスローすることは常に間違っています。
void g8() {
try {
char* e = new char[10];
memcpy(e,"test",5);
throw e; //抛出局部指针
delete[] e; //被跳过
e = nullptr;
} catch (char* e) { // 指针异常对象处理
std::cout << std::string(e);
//再处理e指针释放已经晚了
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
ローカル オブジェクトへのポインターがスローされ、処理コードが別の関数内にある場合、ポインターが指すオブジェクトは、処理コードの実行時に存在しなくなります。処理コードが同じ関数内にある場合でも、ポインターが指すオブジェクトがキャッチに存在することを確認する必要があります。ポインターがキャッチの前に終了したブロック内のオブジェクトを指している場合、ローカル オブジェクトはキャッチの前に破棄されます。
ポインターをスローすることは一般的に悪い考えです。ポインターをスローするには、ポインターが指すオブジェクトが、対応する処理コードが存在する場所に存在する必要があります。
2.11 カスタム例外は、標準の例外クラスから継承するのが最適です
実際には、多くのアプリケーションは、基本型が何らかの継承階層に由来する式をスローします。標準例外は継承階層で定義され、標準例外クラスは多くのアプリケーションで使用できます。さらに、例外クラスまたは中間基本クラスから追加の型を派生させることにより、例外階層が拡張されます。これらの新しく派生したクラスは、アプリケーション ドメインに固有の例外の種類を表すことができます。例外タイプをカスタマイズする場合は、標準例外に基づいて例外クラスを作成するのが最善です。
class myExcep : public exception {};
class myExcep : public runtime_error{};
.....
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
アプリケーション固有の例外の種類は、標準の例外クラスから派生することによって定義されます。他の階層と同様に、例外クラスはレイヤーで編成されていると考えることができます。層が深くなるにつれて、各層はより具体的な異常になります。たとえば、階層の最初で最も一般的なレベルは例外クラスによって表され、このタイプのオブジェクトがキャッチされると、何かが間違っていたことがわかります。2 番目のレイヤーでは、例外を 2 つの大きなカテゴリ (実行時エラーとロジック エラー) に分類します。ユーザー定義の例外クラスの例外を直接継承するのではなく、それらを細分化して、より特化したレイヤーでイベントを表現することをお勧めします。たとえば、myExcep クラスは、実行時に問題が発生する可能性のあるアプリケーション固有のものを表します。
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
void myExcepf()
{
try{
//our code
}catch (const myExcep& e){
}catch (const std::runtime_error& e){
}catch (const std::exception& e){
}
};
ユーザーは、標準ライブラリ クラスと同じ方法で独自の例外クラスを使用します。プログラムの一部は、これらの型のいずれかのオブジェクトをスローし、プログラムの別の部分は、示された問題をキャッチして処理します。
また、静的型付けでオブジェクトをスローすることも問題ではありません。例外がスローされると、スローされるオブジェクトは通常、スローの時点で構築されます。これは何が問題だったかを表すため、正確な例外の種類がわかります。
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
}
2.12 例外処理の「スタック巻き戻し」
例外がスローされると、現在の関数の実行が中断され、一致する catch 句が検索されます。まず、throw 自体が try ブロック内にあるかどうかをチェックし、そうであれば、その catch に関連付けられた catch 句をチェックして、そのうちの 1 つがスローされたオブジェクトと一致するかどうかを確認します。一致するキャッチが見つかった場合は例外を処理し、そうでない場合は現在の関数を終了し (現在の関数の内部を解放してローカル オブジェクトを取り消します)、呼び出し元の関数で検索を続けます。
例外をスローする関数の呼び出しが try ブロック内にある場合、その try に関連付けられた catch 句がチェックされます。一致する catch が見つかった場合、例外が処理されます。一致する catch が見つからない場合、呼び出し元の関数も終了し、この関数を呼び出した関数内で検索を続けます。
スタックの巻き戻しと呼ばれるこのプロセスは、例外の catch 句が見つかるまで、ネストされた関数呼び出しのチェーンを続けます。例外を処理できる catch 句が見つかる限り、catch 句に入り、処理コードで実行を継続します。catch が終了すると、try ブロックに関連付けられた最後の catch 句の直後から実行が続行されます。
void f()
{
try{
std::invalid_argument e("test");
throw e; //主动抛出一个异常
}catch (const std::exception& e){ //[1]
//类型不匹配被跳过
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {//[2]
// ...
} catch (const std::runtime_error& e) {//[3]
// ...
} catch (const std::exception& e) {//[4]
// ...
} catch (...) {//[5]
// ...,这里捕获
}
//terminate //[6]
}
スタックの巻き戻し中に、throw を含む関数と、場合によっては呼び出しチェーン内の他の関数から早期に終了します。一般に、これらの関数は、関数を終了するときに破棄できるローカル オブジェクトを作成しています。例外が原因で関数を終了する場合、コンパイラは、ローカル オブジェクトが適切に割り当て解除されるようにします。各関数が終了すると、そのローカル ストレージが解放され、メモリが解放される前に、例外が発生する前に作成されたすべてのオブジェクトの割り当てが解除されます。ローカル オブジェクトがクラス型の場合、オブジェクトのデストラクタが自動的に呼び出されます。通常、コンパイラは組み込み型のオブジェクトを破棄しません。
スタックの巻き戻し中に、ローカル オブジェクトによって使用されたメモリが解放され、クラス タイプのローカル オブジェクトのデストラクタが実行されます。ブロックがリソースを直接割り当て、リソースが解放される前に例外が発生した場合、リソースはスタックの巻き戻し中に解放されません。たとえば、ブロックは new を呼び出すことで動的にメモリを割り当てることができます。例外が原因でブロックが終了した場合、コンパイラはポインタを削除せず、割り当てられたメモリは解放されません。
クラス型のオブジェクトによって割り当てられたリソースは、通常、適切に割り当て解除されます。ローカル オブジェクトのデストラクタが実行されます。クラス タイプのオブジェクトによって割り当てられたリソースは、通常、デストラクタによって解放されます。例外が発生した場合は、クラス管理のリソース割り当てのプログラミング手法を使用することをお勧めします。
プログラムが例外の処理に失敗することはありません。キャッチされていない例外がある場合、プログラムはプログラムを終了します。例外は、プログラムが正常に実行を継続できないほど重要なイベントです。一致する catch が見つからない場合、プログラムはライブラリ関数 terminate を呼び出します。
2.13 クラスの構築、デストラクタ、例外処理
デストラクタは決して例外をスローしてはならず、デストラクタはスタックの巻き戻し中に実行されることがよくあります。デストラクタの実行中に、例外がスローされましたが、まだ処理されていません。このプロセス中にデストラクタ自体が新しい例外をスローすると、標準ライブラリの終了関数が呼び出されます。一般に、terminate 関数は abort 関数を呼び出して、プログラム全体を強制的に異常終了させます。
終了関数はプログラムを終了するため、通常、デストラクタが例外を引き起こす可能性のあることを行うのは非常に悪い考えです。実際には、デストラクタがリソースを解放するため、例外がスローされる可能性は低くなります。標準ライブラリ型はすべて、デストラクタが例外をスローしないことを保証します。型をカスタマイズするときは、古い例外を上書きしないように、デストラクタの後に例外をスローしないことをお勧めします。
例外はコンストラクターと同じであり、デストラクタとは異なり、コンストラクター内で行われることは、多くの場合、例外をスローします。関数オブジェクトの構築中に例外が発生した場合、オブジェクトは部分的にのみ構築され、一部のメンバーは初期化されていて、他のメンバーは例外が発生する前に初期化されていない可能性があります。オブジェクトが部分的にしか構築されていない場合でも、構築されたメンバーが適切に破棄されることが保証されます。
class ExcepTest
{
public:
ExcepTest();
~ExcepTest();
private:
char* p1;
char* p2;
std::string *p3;
std::string *p4;
};
ExcepTest::ExcepTest()
{
try {//防止构造时异常出现内存泄漏
//do something
}
catch (...) {
delete p4; p4 = nullptr;//不良排版,为了节省行数显示
delete p3; p3 = nullptr;
delete p2; p2 = nullptr;
delete p1; p1 = nullptr;
}
}
ExcepTest::~ExcepTest()
{
if (nullptr != p4) { delete p4; p4 = nullptr;}//不良排版,为了节省行数显示
if (nullptr != p3) { delete p3; p3 = nullptr;}
if (nullptr != p2) { delete p3; p3 = nullptr;}
if (nullptr != p1) { delete p1; p3 = nullptr;}
}
例外は、コンストラクターで、またはコンストラクター初期化子の処理中に発生する可能性があります。コンストラクター初期化子は、コンストラクター関数本体に入る前に処理されます。コンストラクター関数本体内の catch 句は、コンストラクター初期化の処理中に発生する可能性がある例外を処理できません。コンストラクターの初期化子からの例外を処理するには、コンストラクターを関数の try ブロックとして記述する必要があります。一連の catch 句は、関数テスト ブロックを使用して関数と統合できます。
class ExcepTest2
{
public:
//构造函数要处理来自构造函数初始化式的异常,最好是将构造函数编写为函数测试块。
ExcepTest2(char* pc_, int* pi_)
try : p1(pc_),p2(pi_){
}
catch(const std::exception& e)
{
destroy();
std::cerr << e.what() << '\n';
};
~ExcepTest2(){
destroy();
};
private:
void destroy(){
if(nullptr!=p2){ delete p2; p2 = nullptr;};
if(nullptr!=p1){ delete p1; p1 = nullptr;};
}
private:
char* p1;
int* p2;
};
同様に、配列または他のコンテナー型の要素を初期化するときに例外が発生する可能性があり、構築された要素が適切に破棄されることも保証されます。
2.14 例外の再スロー
1 回のキャッチで例外を完全に処理できない可能性があります。修正アクションを実行した後、catch は、例外が関数呼び出しチェーンの上位にある関数によって処理される必要があると判断し、catch は再スローによって関数呼び出しチェーンの上位に例外を渡す場合があります。再スローは、タイプまたは式が後に続かないスローです。
throw;
空の throw ステートメントは例外オブジェクトを再スローします。例外オブジェクトは、catch または catch から呼び出される関数にのみ表示されます。処理コードが非アクティブなときに空のスローが発生すると、終了関数が呼び出されます。
class my_error : public std::exception
{
public:
enum status{client_error,server_error,char_error,port_error};
void reset_status(status st_){ st = st_;};
private:
status st;
};
void g10()
{
try {
throw my_error();// 抛出 my_error
} catch (my_error &eObj) { // specifier is a reference type
//异常判断,继续排除
eObj.reset_status(my_error::server_error);// 修改异常对象
std::cout <<"my_error reset\n";
throw ; // 直接抛给上层调用
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g11()
{
try {
g10();
}catch(...)
{
std::cout <<"error from g10\n";
}
}
//
g11();
//out log
my_error reset
error from g10
再スローは独自の例外を指定しませんが、例外オブジェクトは引き続きチェーンに渡され、スローされる例外は元の例外オブジェクトであり、catch パラメーターではありません。catch パラメーターが基本クラス型の場合、再スローする式によってスローされる実際の型はわかりません。これは、catch パラメーターの静的型ではなく、例外オブジェクトの動的型に依存します。たとえば、基本型パラメーターを使用した catch からの再スローは、実際には派生型のオブジェクトをスローする場合があります。一般に、catch はその仮パラメーターを変更できます。仮パラメーターを変更した後に catch が例外を再スローする場合、それらの変更は、例外指定子が参照である場合にのみ伝搬されます。
catch(...) は、再スローする式と組み合わせて使用されることが多く、catch は可能なローカル作業を実行してから、例外を再スローします。
void g12() {
try {
// 异常捕获触发
}
catch (...) {
// 不再本函数处理,抛给上层函数统一处理
throw;
}
}
2.15 クラスによるリソース割り当ての管理
デストラクタが正しく実行されるようにするため、プログラムをより例外に対して安全にします (例外に対して安全とは、例外が発生してもプログラムが正しく動作することを意味します)。この場合、デストラクタの「安全性」は、「例外が発生した場合、割り当てられたリソースは適切に割り当て解除される」という保証に由来します。リソースの割り当てと解放をクローズするクラスを定義することにより、リソースが正しく解放されるようにすることができます。この手法は、「リソース割り当ての初期化」、または略して RAII と呼ばれることがよくあります。
unique_ptr (C++11) 拥有独有对象所有权语义的智能指针 (类模板)
shared_ptr (C++11) 拥有共享对象所有权语义的智能指针 (类模板)
weak_ptr (C++11) 到 std::shared_ptr 所管理对象的弱引用 (类模板)
auto_ptr (C++17中移除) 拥有严格对象所有权语义的智能指针 (类模板)
リソース管理クラスは、コンストラクタがリソースを割り当て、デストラクタがそれらを解放するように設計する必要があります。リソースを割り当てたい場合は、このタイプのオブジェクトを定義します。例外が発生しなければ、リソースを取得したオブジェクトが範囲外になった時点でリソースが解放されます。さらに重要なことに、オブジェクトが作成された後、スコープ外に出る前に例外が発生した場合、コンパイラは、オブジェクトが定義されたスコープの巻き戻しの一環としてオブジェクトが破棄されることを保証します。
3. 例外処理補助キーワード
3.1 noexcept 指定子
「c/c++ 開発、やむを得ないカスタム クラス型 (第 7 部)」を参照してください。
3.2 動的例外記述スロー
動的例外の説明のスロー (c++17 より前) には、関数によって直接または間接的にスローされる可能性がある例外が一覧表示されます。
- 関数の宣言がその動的例外指定で型 T をリストする場合、関数はその型またはその型から派生した型の例外をスローする可能性があります。
- 型識別子のリストを持たない動的例外宣言 (つまり、throw()) はスローされません。
- 非スローの動的例外仕様を持つ関数は、例外を許可しません。
- 不完全な型、ポインター、または cv void* 以外の不完全な型への参照、および右辺値の参照型は、例外仕様に表示できません。
- 配列型と関数型が使用されている場合、それらは対応するポインター型に調整されます。
- パラメーター パックが許可されます。(C++11以降)
- 動的例外宣言は、関数型の一部とは見なされません。
- 関数が例外仕様にリストされていないタイプの例外をスローした場合、関数 std::unexpected が呼び出されます。
- デフォルトでは、この関数は std::terminate を呼び出しますが、(std::set_unexpected を介して) std::terminate を呼び出すか例外をスローする可能性のあるユーザー提供の関数に置き換えることができます。
- 例外仕様が std::unexpected からスローされた例外を受け入れる場合、スタックの巻き戻しは通常どおり続行されます。受け入れられないが、例外仕様で std::bad_exception が許可されている場合、std::bad_exception がスローされます。それ以外の場合は、std::terminate が呼び出されます。
#include <exception>
#include <assert.h>
static_assert(__cplusplus < 201703,"ISO C++17");
class X {};
class Y {};
class Z : public X {};
class W {};
void func1() throw(X, Y)
{
int n = 0;
if (n) throw X(); // OK
if (n) throw Z(); // OK
throw W(); // 将调用 std::unexpected()
}
void throw_test()
{
std::set_unexpected([]
{
std::cout << "unexpected!" << std::endl; // 需要清除缓冲区
std::abort();
});
func1();
}
一般に、関数 try ブロックで throw を積極的かつ明示的に呼び出して例外オブジェクトをスローすることはお勧めしませんが、間接的かつ暗黙的な方法で例外をスローすることはお勧めしません。
3.3 スローと潜在的な例外
すべての関数f
、関数ポインターpf
、およびメンバー関数pmf
ポインタースローされる可能性のある型で構成される「潜在的な例外セット」があります。すべての型のコレクションは、例外がスローされる可能性があることを示します。暗黙的に宣言された特別なメンバー関数 (コンストラクタ、代入演算子、およびデストラクタ) および継承されたコンストラクタ (C++11 以降) の場合、潜在的な例外のセットは、それらが呼び出すすべての関数の潜在的な例外のセットです。 -variant 非静的データ メンバー、コンストラクター/代入演算子/直接基底クラスのコンストラクター/代入演算子/デストラクター (デフォルトの引数式も)、および必要に応じて仮想基底クラス。
各式 e は、一連の潜在的な例外を保持します。e がコア定数式である場合、セットは空です。それ以外の場合、セットは、e のすべての直接部分式 (デフォルトの引数式を含む) の潜在的に例外的なセットと、e の形式に応じて次の別のセットの和集合になります。 .
1) 如果 e 是一个函数调用表达式,令 g 代表被调用的函数、函数指针或成员函数指针,那么:
◦如果 g 的声明使用了动态异常说明,那么添加 g 的潜在异常集合到集合;
◦如果 g 的声明使用了 noexcept(true),那么集合为空;(C++11 起)
◦否则,该集合是所有类型的集合。
2) 如果 e 隐式调用了某个函数(它是运算符表达式且该运算符被重载,是 new 表达式且其分配函数被重载,或是完整表达式且调用了临时量的析构函数),那么该集合是这个函数的集合。
3) 如果 e 是一个 throw 表达式,那么该集合是以其操作数所初始化的异常,或对于重抛出表达式(无操作数者)是所有异常的集合。
4) 如果 e 是对多态类型引用的 dynamic_cast,那么该集合由 std::bad_cast 组成。
5) 如果 e 是应用到多解引用指向多态类型指针的 typeid,那么该集合由 std::bad_typeid 组成。
6) 如果 e 是一个拥有非常量数组大小的 new 表达式,且选择的分配函数拥有非空的潜在异常集合,那么该集合由 std::bad_array_new_length 组成。 (C++11 起)
void f() throw(int); // f() 的集合是“int”
void g(); // g() 的集合是所有类型的集合
struct A { A(); }; // “new A”的集合是所有类型的集合
struct B { B() noexcept; }; // “B()”的集合为空
struct D() { D() throw (double); }; // “new D”的集合是所有类型的集合
3.4 スロー関数とメンバー関数
暗黙的に宣言されたすべてのメンバー関数と継承されたコンストラクター (C++11 以降) には例外の説明があり、メンバー関数宣言の例外の説明は関数パラメーター リストの後にあり、オプションは次のとおりです。
◦如果潜在异常的集合是类型全集,那么隐式异常说明允许所有异常(该异常说明被认为存在,即使它不可用代码表达,且表现如同无异常说明) (C++11 前)是 noexcept(false) (C++11 起)。
◦否则,如果潜在异常的集合非空,那么隐式异常说明列出每个来自该集合的类型
◦否则,隐式异常说明是 throw() (C++11 前)noexcept(true) (C++11 起)。
//
struct A
{
A(int = (A(5), 0)) noexcept;
A(const A&) throw();
A(A&&) throw();
~A() throw(X);
};
struct B
{
B() throw();
B(const B&) = default; // 异常说明是“noexcept(true)”
B(B&&, int = (throw Y(), 0)) noexcept;
~B() throw(Y);
};
int n = 7;
struct D : public A, public B
{
int * p = new (std::nothrow) int[n];
// D 拥有下列隐式声明的成员:
// D::D() throw(X, std::bad_array_new_length);
// D::D(const D&) noexcept(true);
// D::D(D&&) throw(Y);
// D::~D() throw(X, Y);
};
const メンバー関数の宣言では、const 修飾子の後に例外指定が続くことに注意してください。
3.5 例外記述スローと仮想関数
基本クラスの仮想関数の例外の説明は、派生クラスの対応する仮想関数の例外の説明とは異なる場合があります。ただし、派生クラスの仮想関数の例外指定は、対応する基本クラスの仮想関数の例外指定と同じか、それよりも厳しくする必要があります。この制限により、派生クラスの仮想関数が基本クラスの型へのポインターを使用して呼び出された場合、派生クラスの例外仕様によって新しいスロー可能な例外が追加されないことが保証されます。例えば:
class Base {
public:
virtual double f1(double) throw (){};
virtual int f2(int) throw (std::logic_error){};
virtual std::string f3() throw(std::logic_error, std::runtime_error){};
};
class Derived : public Base {
public:
// error: 异常列表范围收窄
// double f1(double) throw (std::underflow_error){};
// ok: 相同异常列表
int f2(int) throw (std::logic_error){};
// ok: 异常列表范围变大
std::string f3() throw (){};
};
派生クラスでの f1 の宣言は正しくありません。その例外指定が、f1 の基本クラス バージョンに対してリストされている例外に追加されているためです。継承階層のユーザーは仕様リストに依存するコードを記述できる必要があるため、派生クラスは例外仕様リストに例外を追加できません。基本クラスのポインターまたは参照を介して関数呼び出しが行われる場合、これらのクラスのユーザーは、基本クラスで指定された例外のみを考慮する必要があります。PS: キーワード noexcept も同様です。
派生クラスによってスローされる例外は、基本クラスによってリストされたものに制限されるため、コードを記述するときにどの例外を処理する必要があるかがわかります。コードは、基本クラスの例外のリストが、仮想関数の派生クラス バージョンがスローできる例外のリストのスーパーセットであるという事実に依存できます。
例外仕様は関数型の一部です。このようにして、関数ポインターの定義で例外仕様を提供することもできます。
void (*pf)(int) throw(runtime_error);
このステートメントは、pf が int 値を受け入れる関数を指していること、その関数が void オブジェクトを返すこと、およびこの関数がタイプ runtime_error の例外のみをスローできることを意味します。例外仕様が提供されていない場合、ポインターは、任意の型の例外をスローできる一致する型の関数を指すことができます。
3.6 std::終了
//定义于头文件 <exception>
void terminate(); //(C++11 前)
[[noreturn]] void terminate() noexcept; //(C++11 起)
次のいずれかの理由でプログラムを続行できない場合、C++ ランタイムは std::terminate() を呼び出します。
1) 未捕捉抛出的异常(此情况下是否进行任何栈回溯是实现定义的)
2) 在处理仍未经由异常捕捉的异常时(例如由某局部对象的析构函数,或构造 catch 子句参数的复制构造函数抛出),由异常处理机制所直接调用
3) 静态或线程局域 (C++11 起)对象的构造函数或析构函数抛出异常
4) 以 std::atexit 或 std::at_quick_exit (C++11 起)注册的函数抛出异常
5) 违反动态异常说明,并执行了 std::unexpected 的默认处理函数 (C++17 前)
6) std::unexpected 的非默认处理函数抛出了违背先前所违背动态异常规定的异常,若这种规定不包含 std::bad_exception (C++17 前)
7) 违反 noexcept 说明(此情况下是否进行任何栈回溯是实现定义的)(C++11 起)
8) 为一个不保有被捕获异常的对象调用 std::nested_exception::rethrow_nested(C++11 起)
9) 从 std::thread 的起始函数抛出异常(C++11 起)
10) 可结合的 std::thread 被析构或赋值(C++11 起)
11) std::condition_variable::wait、 std::condition_variable::wait_until 或 std::condition_variable::wait_for 无法达成其前条件(例如若重锁定互斥抛出)(C++11 起)
12) 并行算法所调用的函数经由未捕捉异常退出,且该执行策略指定了终止。(C++17 起)
std::terminate() は、プログラムから直接呼び出すこともできます。
いずれにせよ、std::terminate は現在インストールされている std::terminate_handler を呼び出します。デフォルトの std::terminate_handler は std::abort を呼び出します。
*若析构函数在栈回溯时重设 terminate_handler ,且后面的回溯导致调用 terminate ,则在 throw 表达式的结尾安装的处理函数会得到调用。(注意:重抛出是否应用新处理函数是有歧义的) (C++11 前)
*若析构函数在栈回溯时重设 terminate_handler ,则若后面的栈回溯导致调用 terminate ,调用哪个处理函数是未指定的。 (C++11 起)
3.7 std::予期しない
std::unexpected() は、動的例外指定に違反した場合に C++ ランタイムによって呼び出されます。このタイプの例外は、例外指定がこのタイプの例外を禁止している関数によってスローされます。
//定义于头文件 <exception>
void unexpected(); //(C++11 前)
[[noreturn]] void unexpected(); //(C++11 起)(弃用)(C++17 中移除)
std::unexpected() は、プログラムから直接呼び出すことができます。いずれの場合も、std::unexpected は現在インストールされている std::unexpected_handler を呼び出します。デフォルトの std::unexpected_handler は std::terminate を呼び出します。
*若析构函数在栈回溯期间重置 unexpected_handler 且之后的回溯导致调用 unexpected ,则将调用于 throw 表达式结尾安装的处理函数。(注意:重抛出是否应用新的处理函数是有歧义的) (C++11 前)
*若析构函数在栈回溯期间重置 unexpected_handler ,则若之后的回溯导致调用 unexpected ,则调用哪个处理函数是未指定的。
第四に、アサーションについて - マクロ assert
4.1 マクロアサートの定義
C/C++ は、プログラムのデバッグ時によく使用されるマクロ アサートを定義します。
//定义于头文件 <cassert>或 <assert.h>
#ifdef NDEBUG
#define assert(condition) ((void)0)
#else
#define assert(condition) /*implementation defined*/
#endif
プログラムを実行すると、括弧内の式が評価され、式が FALSE (0) の場合、プログラムはエラーを報告して実行を終了します。式が 0 でない場合、実行は次のステートメントから続行されます。上記のように、マクロ assert の定義は、標準ライブラリで定義されていない NDEBUG という別のマクロに依存しています。
4.2 マクロアサートは NDEBUG マクロと組み合わせて使用されます
NDEBUG という名前のマクロが定義されていない場合、assert はその引数 (スカラー型でなければならない) を 0 と比較して等しいかどうかを調べます。等しい場合、assert は実装固有の診断を標準エラーに出力し、std::abort を呼び出します。診断情報には、式のテキスト、定義済み変数 __func__ および (C++11 以降) 定義済みマクロ __FILE__、__LINE__ の値を含める必要があります。
NDEBUG は、最後の定義または再定義のアサート位置 (つまり、ヘッダー ファイル <cassert> または <assert.h> が含まれる) で定義され、式 assert(E) は定数部分式であることが保証されます。は、コンテキストに従って E が bool に変換された後、true に評価される定数部分式です。
void assertion_test(void)
{
assert(1+1==2);
std::cout << "Execution continues past the first assert\n";
assert(1+1==3); //断言捕获,直接调用abort告警及退出
std::cout << "Execution continues past the second assert\n";
//do other
std::cout << "dosomething!\n"; //程序退出,还得到执行
}
int main(int argc, char* argv[])
{
assertion_test();
std::cout <<"main finish!\n";
return 0;
}
コンパイルとテスト g++ main.cpp test*.cpp -o test.exe -std=c++11, output
g++ main.cpp test*.cpp -o test.exe -std=c++11 -DNDEBUG, 出力のコンパイルとテスト
そのため、アサーション マクロ assert は、マクロ DNDEBUG と組み合わせて使用され、主にデバッガでの例外キャプチャに使用されます. ほとんどの場合、ポインターの検証など、関数エントリの仮パラメーターの監視に使用されます.
struct TestA
{
/* data */
char* pc;
};
void test_assert(TestA* ptr)
{
if(nullptr==ptr) return; //可预见的直接函数自行判定
assert(nullptr!=ptr->pc); //形参内部的采用断言检测
//do other
}
4.3 マクロ assert 置換式の例外に注意する
assert(E) は関数のようなマクロであるため、定数式 E がプリコンパイルによって置き換えられると、括弧で保護されず、コードの要望を正しく実装できないという事実に注意する必要があります。
#include <complex>
void test_assert_def(void)
{
std::complex<double> c;
// assert(c == std::complex<double>{0, 0}); // 错误,未被括号保护的逗号都被转译成宏参数的分隔符
assert((c == std::complex<double>{0, 0})); // OK
};
assert を使用することの欠点は、頻繁な呼び出しがプログラムのパフォーマンスに大きく影響し、追加のオーバーヘッドが増加することです。デバッグ後、#include <assert.h> を含むステートメントの前に #define NDEBUG を挿入することでアサート呼び出しを無効にすることができます。サンプル コードは次のとおりです。コンパイル コマンドに -DNDEBUG を追加するか、コンパイル プロジェクトに NDEBUG を追加します。
#include <stdio.h>
#define NDEBUG
#include <assert.h>
4.4 コンパイル アサーション - static_assert
マクロ assert は、主に実行時にアサーションを作成するために使用されます。C++11 標準では、コンパイル時のアサーションを実装するために static_assert マクロが導入されました。static_assert は非常に簡単に使用できます. 2 つのパラメータを受け取ります. 1 つはアサーション式で, 通常はブール値を返す必要があります. もう 1 つは警告メッセージで, 通常は文字列です. このキーワードの使用法は、C++11 標準より前のオープン ソース ライブラリ Boost の組み込み BOOST_STATIC_ASSERT アサーション メカニズムに似ており、式 1/(e) を使用して次のことを決定します。
#define assert_static(e) \
do{ \
enum { assert_static = 1/(e) }; \
)while(0)
static_assert can be used in any namespace. 前処理段階で、static_assert マクロは _Static_assert という名前の C キーワードに展開されます。このキーワードは、最初の引数として定数式を受け入れる「関数呼び出し」のような形式で C コードで使用されます。プログラムがコンパイルされると、コンパイラはこの式を評価し、結果を数字のゼロと比較します。両者が等しい場合、プログラムはコンパイルを終了し、第 2 パラメーターで指定されたエラー情報とアサーション失敗情報を組み合わせて出力します。2 つが等しくない場合、プログラムは正常にコンパイルされ、キーワードに対応する C コードは対応する機械語命令を生成しません。
int bit_deal (int& a) (
static_assert(sizeof(int) >=4,"the parameters of bit should have at least 4 width.");
);
プログラムは、プログラムのコンパイル時に、そのプラットフォームでの int 型の幅が 4 バイト以上である必要があることを制限し、コンパイルは終了し、対応するエラー メッセージが出力されます。
error: static assertion failed: "the parameters of bit should have at least 4 width."
このようなエラー メッセージは非常に明確であり、プログラマーがトラブルシューティングを行う際にも非常に役立ちます。一般的に言えば、静的アサーションを使用して、プログラムを実行する前に満たす必要がある一連の要件をチェックします。上記の例のように、静的アサーションにより、開発者は現在のプラットフォームで実行した場合にプログラムが正常に動作するかどうかを事前に知ることができます。
char 型のデフォルトのシンボリック性を判断したり、ポインター型の幅が int 型の幅と等しいかどうかを判断したり、特定の構造体のサイズが期待される要件を満たしているかどうかを判断したりするなど、同様のユース ケースが多数あります。等々。これらは、c/c++ プログラムの正確性に影響を与える可能性のある要因であり、静的アサーションを通じて、コンパイル時に事前に検出できます。
通常は、 static_assert を関数本体の外に記述することをお勧めします。これにより、コード リーダーが static_assert がユーザー定義関数ではなくアサーションであることを見つけやすくなります。
また、static_assert のアサーション式の結果は、コンパイル時に計算できる式、つまり定数式でなければならないことに注意してください。リーダーが変数を使用すると、次のようなエラーが発生します。
int bit_deal (int a) (
static_assert( a>=1,"the parameters of a should >=1.");
);
上記ではパラメータ変数 a を使用しているため、static_assert はコンパイルできません。変数をチェックする必要がある場合は、ランタイム チェックを実装し、アサート マクロを使用する必要があります。
5. 例外処理の提案
【1】異常と非異常を見分ける
ビジネス ロジックの実装エラーをプログラム エラーのせいにしないでください。例えば、あるデータが閾値に達した場合、条件判断でそのビジネスロジック処理を実現し、例外をスローしてプログラムを強制的に実行させないようにする必要があります。エラー処理 (例外やエラー コードなど) とテクニック (セキュリティ保証、キーワード、事前条件、事後条件など)を適切に区別する必要があります。.
[2] 例外処理が正確に必要であり、怠惰ではないことを確認してください
例外処理は効率コストをもたらします. 例外処理を効果的に使用するには、ビジネス ロジックとコード ロジックを理解する必要があります. このオブジェクトのタイプによって、アクティブにする処理コードが決まります。選択されたハンドラーは、オブジェクト タイプに一致し、例外がスローされた場所に最も近いコール チェーン内のハンドラーです。例外は、関数に引数を渡すのと同様の方法でスローおよびキャッチされます。例外は、非参照パラメーターに渡すことができる任意の型のオブジェクトにすることができます。つまり、その型のオブジェクトをコピーできる必要があります。
[3] 網羅的例外の例外処理ではなく、論理条件処理を使用する
関数実行時に発生するエラー状況が予測でき、条件分岐で対処できる場合は、例外処理を使用する必要はありません。
[4] 例外処理は、デバッグを繰り返した後にいくつかの例外レベルを排除します
プロジェクトの実践では、ビジネスシナリオやビジネスロジックの理解に問題があるため、最初に多くの例外処理レベルを導入することは通常不可能ですが、ビジネスの消化とバージョンの反復により、多くの例外が事前に抑制されるため、例外処理レベルも高く設定する必要があります。同時取り外しです。
6. デモソースコード
コンパイル手順: g++ main.cpp test*.cpp -o test.exe -std=c++11 (オプション: -DNDEBUG)
main.cpp
#include "test1.h"
#include <iostream>
int main(int argc, char* argv[])
{
assertion_test();
exception_test();
std::cout <<"main finish!\n";
return 0;
}
test1.h
#ifndef _TEST_1_H_
#define _TEST_1_H_
void assertion_test(void);
void exception_test(void);
#endif //_TEST_1_H_
test1.cpp
#include "test1.h"
#include <cassert>
#include <iostream>
void assertion_test(void)
{
assert(1+1==2);
std::cout << "Execution continues past the first assert\n";
assert(1+1==3); //断言捕获,直接返回main退出
std::cout << "Execution continues past the second assert\n";
//do other
std::cout << "dosomething!\n"; //程序退出,还得到执行
}
struct TestA
{
/* data */
char* pc;
};
void test_assert(TestA* ptr)
{
if(nullptr==ptr) return; //可预见的直接函数自行判定
assert(nullptr!=ptr->pc); //形参内部的采用断言检测
//do other
}
#include <complex>
void test_assert_def(void)
{
std::complex<double> c;
// assert(c == std::complex<double>{0, 0}); // 错误,未被括号保护的逗号都被转译成宏参数的分隔符
assert((c == std::complex<double>{0, 0})); // OK
};
class myExcep : public std::runtime_error {
public:
explicit myExcep (const std::string &s) : std::runtime_error(s) { };
};
void myExcepf()
{
try{
//our code
}catch (const myExcep& e){
}catch (const std::runtime_error& e){
}catch (const std::exception& e){
}
};
void f()
{
try{
std::invalid_argument e("test");
throw e;
}catch (const std::exception& e){
}
}
void g()
{
try {
f();
} catch (const std::overflow_error& e) {
// ...
} catch (const std::runtime_error& e) {
// ...
} catch (const std::exception& e) {
// ...
} catch (...) {
// ...
}
}
void g1()
{
try {
//...code
} catch (const std::exception& e) {
// 若 f() 抛 std::runtime_error 则执行
} catch (const std::runtime_error& e) {
// 死代码!
}
}
#include <string>
void g2()
{
try {
std::string("abc").substr(10); // 抛出 std::length_error
// } catch (std::exception e) { // 从 std::exception 基类复制初始化
// std::cout << e.what(); // 丢失来自 length_error 的信息
// }
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what(); // 打印来自 length_error 的信息
}
}
class A1{};
class A2{};
void g3()
{
int i=0;
label:
try {
A1 a1;
try {
A2 a2;
if(i<2)
goto label; // 销毁 a2,然后销毁 a1,再跳到 label
} catch (...) { } // 捕捉来自 a2 析构函数的异常
} catch (...) { } // 捕捉来自 a1 析构函数的异常
}
#include <iostream>
#include <vector>
void g4() {
try {
std::cout << "Throwing an integer exception...\n";
throw 42; //主动抛出int型对象
} catch (int i) {
std::cout << " the integer exception was caught, with value: " << i << '\n';
}
//
try {
std::cout << "Creating a vector of size 5... \n";
std::vector<int> v(5);
std::cout << "Accessing the 11th element of the vector...\n";
std::cout << v.at(10); // vector::at() 抛出 std::out_of_range
} catch (const std::exception& e) { // 按基类的引用捕获
std::cout << " a standard exception was caught, with message '"
<< e.what() << "'\n";
}
}
class exception_def: public std::exception{};
void f(const exception_def& e)
{
//code
}
void g5() {
try {
exception_def e;
throw e;
} catch (const exception_def& e) { // 多态对象派生类的引用
f(e);
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g6() {
try {
int excep[2]={1,2};
throw excep; //转换为指针
} catch (int e[]) { // 转换为指针
//
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class OperObj
{
public:
void operator()(){};
};
void g7() {
try {
OperObj e;
throw e; //转换为指针
} catch (OperObj& e) { // 转换为指针
e(); //
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class exception_PTR: public std::exception{};
void g8() {
std::exception *eptr= new exception_PTR();
try {
//do something
throw eptr; //抛出局部指针
} catch (const std::exception* e) { // 多态对象基类的引用
//只能使用基类的成员函数,派生类内重载的不可用
std::cout << e->what();
}
if(nullptr!=eptr){
delete eptr;
eptr = nullptr;
}
}
void g9() {
try {
char* e = new char[10];
memcpy(e,"test",5);
throw e; //抛出局部指针
delete[] e; //被跳过
e = nullptr;
} catch (char* e) { // 指针异常对象处理
std::cout << std::string(e);
//再处理e指针释放已经晚了
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
class ExcepTest
{
public:
ExcepTest();
~ExcepTest();
private:
char* p1;
char* p2;
std::string *p3;
std::string *p4;
};
ExcepTest::ExcepTest()
{
try {//防止构造时异常出现内存泄漏
//do something
}
catch (...) {
delete p4; p4 = nullptr;//不良排版,为了节省行数显示
delete p3; p3 = nullptr;
delete p2; p2 = nullptr;
delete p1; p1 = nullptr;
}
}
ExcepTest::~ExcepTest()
{
if (nullptr != p4) { delete p4; p4 = nullptr;}//不良排版,为了节省行数显示
if (nullptr != p3) { delete p3; p3 = nullptr;}
if (nullptr != p2) { delete p3; p3 = nullptr;}
if (nullptr != p1) { delete p1; p3 = nullptr;}
}
class ExcepTest2
{
public:
ExcepTest2(char* pc_, int* pi_)
try : p1(pc_),p2(pi_){
}
catch(const std::exception& e)
{
destroy();
std::cerr << e.what() << '\n';
};
~ExcepTest2(){
destroy();
};
private:
void destroy(){
if(nullptr!=p2){ delete p2; p2 = nullptr;};
if(nullptr!=p1){ delete p1; p1 = nullptr;};
}
private:
char* p1;
int* p2;
};
class my_error : public std::exception
{
public:
enum status{client_error,server_error,char_error,port_error};
void reset_status(status st_){ st = st_;};
private:
status st;
};
void g10()
{
try {
throw my_error();// 抛出 my_error
} catch (my_error &eObj) { // specifier is a reference type
//异常判断,继续排除
eObj.reset_status(my_error::server_error);// 修改异常对象
std::cout <<"my_error reset\n";
throw ; // 直接抛给上层调用
} catch (const std::exception& e) { // 多态对象基类的引用
std::cout << e.what();
}
}
void g11()
{
try {
g10();
}catch(...)
{
std::cout <<"error from g10\n";
}
}
void g12() {
try {
// 异常捕获触发
}
catch (...) {
// 不再本函数处理,抛给上层函数统一处理
throw;
}
}
#include <exception>
#include <assert.h>
static_assert(__cplusplus < 201703,"ISO C++17");
class X {};
class Y {};
class Z : public X {};
class W {};
void func1() throw(X, Y)
{
int n = 0;
if (n) throw X(); // OK
if (n) throw Z(); // OK
throw W(); // 将调用 std::unexpected()
}
void throw_test()
{
std::set_unexpected([]
{
std::cout << "unexpected!" << std::endl; // 需要清除缓冲区
std::abort();
});
func1();
}
class Base {
public:
virtual double f1(double) throw (){};
virtual int f2(int) throw (std::logic_error){};
virtual std::string f3() throw(std::logic_error, std::runtime_error){};
};
class Derived : public Base {
public:
// error: 异常列表范围收窄
// double f1(double) throw (std::underflow_error){};
// ok: 相同异常列表
int f2(int) throw (std::logic_error){};
// ok: 异常列表范围变大
std::string f3() throw (){};
};
void exception_test(void)
{
g7();
g11();
throw_test();
}