Sapporocpp#2 exception-primer

Preview:

DESCRIPTION

sapporo.cpp #2

Citation preview

例外安全入門@hotwatermorning

自己紹介

● @hotwatermorning● はてなid:heisesswasser● 大学生● DTMやってます● C++が好きです● 「プログラミングの魔導少女」では

「RangeとPStade.Oven」という記事を書かせていただきました。

C++erなら

誰しもメモリーリークに

悩まされたことがあるはず

今回のセッションは

その悩みを

軽減する

軽減する

かもしれない

本日のレシピ

1.例外安全とは?

2.例外安全性の種類

3.例外安全なコードを書くには

4.例外安全にするための技法

本日のレシピ

1.例外安全とは?

2.例外安全性の種類

3.例外安全なコードを書くには

4.例外安全にするための技法

●いわゆる、エラーハンドリング/例外ハンドリング関連の内容は含んでおりません。

本日のレシピ

1.例外安全とは?

2.例外安全性の種類

3.例外安全なコードを書くには

4.例外安全にするための技法

●いわゆる、エラーハンドリング/例外ハンドリング関連の内容は含んでおりません。●というかそれは僕も知りたいので誰か教えt(ry

1.例外安全とは?

例外安全とは?

「あるコード内を実行中の失敗が、メモリリーク、格納データの不整合、不正な出力などの有害な効果を生じないとき、そのコード片は例外安全であると言う。」“例外処理”

http://ja.wikipedia.org/wiki/%E4%BE%8B%E5%A4%96%E5%87%A6%E7%90%86

例外安全とは?

「例外安全なプログラミングとは、 例外を投げる可能性があるコードが実際に例外を投げた場合に、 プログラムの状態が壊れずリソースもリークしないように作るプログラミングのことを言います。 」“例外安全なプログラミング”

http://www.kmonos.net/alang/d/2.0/exception-safe.html

例外安全とは?

● 例外が発生しても適切に対処できるようなコードを書く。

● 適切に対処しないと・・・‣ メモリリーク、リソースリーク‣ 中途半端な操作の完了‣ デストラクタから例外が投げられる

と・・・

例外安全でないコードの例(1)

void SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; read_data(data_, n);}

例外安全でないコードの例(1)

void SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; //←ここや read_data(data_, n);}

例外安全でないコードの例(1)

void SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; //←ここや read_data(data_, n);//←ここで}

例外安全でないコードの例(1)

void SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; //←ここや read_data(data_, n);//←ここで} //例外が投げられると...??

例外安全でないコードの例(1)

void SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; //←ここや read_data(data_, n);//←ここで} //例外が投げられると...??

SampleClassの状態が壊れてしまう!!

例外安全でないコードの例(2)

SampleClass2 & SampleClass2::operator= (SampleClass2 const &rhs){ data1_ = rhs.data1_; data2_ = rhs.data2_; data3_ = rhs.data3_; data4_ = rhs.data4_; return *this;}

例外安全でないコードの例(2)

SampleClass2 & SampleClass2::operator= (SampleClass2 const &rhs){ data1_ = rhs.data1_; data2_ = rhs.data2_;//このうち data3_ = rhs.data3_; data4_ = rhs.data4_; return *this;}

例外安全でないコードの例(2)

SampleClass2 & SampleClass2::operator= (SampleClass2 const &rhs){ data1_ = rhs.data1_; data2_ = rhs.data2_;//このうち data3_ = rhs.data3_;//どれかで data4_ = rhs.data4_; return *this;}

例外安全でないコードの例(2)

SampleClass2 & SampleClass2::operator= (SampleClass2 const &rhs){ data1_ = rhs.data1_; data2_ = rhs.data2_;//このうち data3_ = rhs.data3_;//どれかで data4_ = rhs.data4_;//例外が起きたら return *this;}

例外安全でないコードの例(2)

SampleClass2 & SampleClass2::operator= (SampleClass2 const &rhs){ data1_ = rhs.data1_;//これはどうなる? data2_ = rhs.data2_;//このうち data3_ = rhs.data3_;//どれかで data4_ = rhs.data4_;//例外が起きたら return *this;}

例外安全なコードじゃないと

例外が起きたときにきちんと対処できない

よし、

例外安全なコードを書こう!

と、そのまえに

2.例外安全性の種類

例外安全性の種類

●例外を投げない保証●強い例外安全●基本的な例外安全●例外安全保証なし

例外安全性

例外安全性の種類

●例外を投げない保証●強い例外安全●基本的な例外安全●例外安全保証なし

但し・・・

例外安全性の種類

●例外を投げない保証●強い例外安全●基本的な例外安全●例外安全保証なし

コスト…

例外安全性の種類

● 例外安全保証なし

例外安全性の種類

● 例外安全保証なし● その関数や呼び出し先で例外が起きるとリソースがリークしてしまうかもしれない・・・

例外安全性の種類

● 例外安全保証なしvoid SampleClass::ReadData(int n){ delete [] data_; data_ = new T[n]; read_data(data_, n);}

例外安全性の種類

● 基本的な例外安全の保証

例外安全性の種類

● 基本的な例外安全の保証● 例外が投げられても、いかなるリソースもリークしない。

例外安全性の種類

● 基本的な例外安全の保証● 例外が投げられても、いかなるリソースもリークしない。

● 副作用は出るかもしれない(データの変更や出力など。)

例外安全性の種類

● 基本的な例外安全の保証● 例外が投げられても、いかなるリソースもリークしない。

● コンテナの中で例外が発生した場合、一貫性は保っているが、予測可能な状態とは限らない。

● なのでその後、削除や再利用はできる。

例外安全性の種類

● 基本的な例外安全の保証void SampleClass::ReadData(int n){ delete [] data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); } catch(...) { delete [] data_; data_ = 0; }}

例外安全性の種類

● 強い例外安全の保証

例外安全性の種類

● 強い例外安全の保証● 例外が起きたなら、全ての変更はロールバックされる

● 完全な成功か、例外による完全な失敗かの2つの状況になる

例外安全性の種類

● 強い例外安全の保証void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

例外安全性の種類

● 例外を投げない保証(no throw)

例外安全性の種類

● 例外を投げない保証(no throw)● 操作は全て正しく完了されることが保証される。

例外安全性の種類

● 例外を投げない保証(no throw)● 操作は全て正しく完了されることが保証される。

● 例)基本データ型の代入などは例外が起きない

例外安全性の種類

● 例外を投げない保証(no throw)● 操作は全て正しく完了されることが保証される。

● 例)基本データ型の代入などは例外が起きない

● 呼び出し先も例外を投げない保証ができなければならない

例外安全性の種類

● 例外を投げない保証(no throw)void foo(){ int n1 = 0x10; int n2 = n1; int *pn1 = &n1; int *pn2 = pn1;}

3.例外安全なコードを書くには

例外安全なコードを書くには

● ある実行パス中の例外安全性は、その工程で一番低いものになる。

例外安全なコードを書くには

● ある実行パス中の例外安全性は、その工程で一番低いものになる。

● どゆこと?

例外安全なコードを書くには

int baz() { return bar() * bar(); }void foo(){ int n1 = 1; //例外安全なコード int n2 = bar(); //例外安全でないコード int n3 = 3; //例外安全なコード int n4 = baz(); //例外安全でないコード}

例外安全なコードを書くには

void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

(さっきの「強い例外安全」のサンプルコード)

例外安全なコードを書くには

void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); //←もしこの関数が delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

(さっきの「強い例外安全」のサンプルコード)

例外安全なコードを書くには

void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); //←もしこの関数が delete [] tmp; //例外安全じゃないと } catch(...) { delete [] data_; data_ = tmp; }}

(さっきの「強い例外安全」のサンプルコード)

例外安全なコードを書くには

void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); //←もしこの関数が delete [] tmp; //例外安全じゃないと } catch(...) { //ReadData関数は delete [] data_; d...//完全な例外安全とは } //言えなくなる}

(さっきの「強い例外安全」のサンプルコード)

例外安全なコードを書くには

● ある実行パス中の例外安全性は、その工程で一番低いものになる。

● 呼び出し先の例外安全性にも左右されてしまう。

例外安全なコードを書くには

● ある実行パス中の例外安全性は、その工程で一番低いものになる。

● 呼び出し先の例外安全性にも左右されてしまう。

● プログラムの中で基本的な機能であるほど、しっかりした例外安全性を実装していなければならない。(コンテナなど)

例外安全なコードを書くには

● ある実行パス中の例外安全性は、その工程で一番低いものになる。

● 呼び出し先の例外安全性にも左右されてしまう。

● プログラムの中で基本的な機能であるほど、しっかりした例外安全性を実装していなければならない。(コンテナなど)

● 標準のコンテナは大体強い保証を満たす

例外安全なコードを書くには

● それぞれの関数で例外を投げうる部分と投げない部分を明確に分離する。

● 本来の処理の成功を確認した時点で、例外を投げない部分を使って、状態の変更と後処理を行うようにする。

例外安全なコードを書くには

● 例外中立について

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; } //例外安全に書けましたね!}

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; } //でもいざ例外が起きたときに}

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; } //ReadDataの呼び出し元は} //ここで起きた例外をなにも知らない

例外安全なコードを書くには

● 例外中立について● コンテナなどで例外が起きると、その例外が起きるまでのコンテキストを知っているのは呼び出し元だけ。

例外安全なコードを書くには

● 例外中立について● コンテナなどで例外が起きると、その例外が起きるまでのコンテキストを知っているのは呼び出し元だけ。

● コンテナでは、自分の行った範囲の例外は安全に処理できるけど、他は無理。

例外安全なコードを書くには

● 例外中立について● コンテナなどで例外が起きると、その例外が起きるまでのコンテキストを知っているのは呼び出し元だけ。

● コンテナでは、自分の行った範囲の例外は安全に処理できるけど、他は無理。

● 適切に処理できるところまでは例外を正しく伝える。

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

例外安全なコードを書くにはvoid SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; //正しくはこう。 }}

例外安全なコードを書くには

● 単一の関数にatomicでない複数の処理があると、例外安全の強い保証をするのは不可能。

● 例)std::coutに出力してからstd::cerrに出力

4.例外安全にするための技法

これまでのことは

いまから紹介する2つのテクニックの

布石に過ぎなかった

布石に過ぎなかった\ばばーん/

例外安全にするための技法

● いままで説明してきたことを上手く実装するためのテクニック

例外安全にするための技法

● いままで説明してきたことを上手く実装するためのテクニック

● Swap

例外安全にするための技法

● いままで説明してきたことを上手く実装するためのテクニック

● Swap● RAII(Resource Acquisition Is Initialization)

例外安全にするための技法

● Swap● 実体を他に作って、全て成功したら、変更したいリソースとSwapする

例外安全にするための技法

● Swap● 実体を他に作って、全て成功したら、変更したいリソースとSwapする

● Swapは、例外を投げない保証を必ず守る

例外安全にするための技法template<typename T>void swap(T &lhs, T &rhs){ T tmp = lhs; lhs = rhs; rhs = tmp;}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //頑張って強い例外安全にしてるけど}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //ロールバック用にいろいろ書いてて}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //例外を投げる部分が}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //例外を投げない部分に囲まれて}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //Try-Catchも入り乱れて}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //ちょっと複雑・・・}

例外安全なコードを書くには

● それぞれの関数で例外を投げうる部分と投げない部分を明確に分離する。

● 本来の処理の成功を確認した時点で、例外を投げない部分を使って、状態の変更と後処理を行うようにする。

例外安全にするための技法

● Swap● 実体を他に作って、全て成功したら、変更したいリソースとSwap

例外安全にするための技法

● Swap● 実体を他に作って、全て成功したら、変更したいリソースとSwap

例外を投げうる部分投げない部分

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = new T[n]; //先に確保 try { read_data(tmp_, n);//そいつで処理 } catch(...) { delete [] tmp; }

std::swap(data_, tmp);//終わったら入れ替え delete [] tmp; //後処理}

例外安全にするための技法

● Swap● 例外を投げうる部分と投げない部分を明確に分離

● 例外を投げる部分が成功したら例外を投げない部分で状態を変更

● ロールバック用のTry-Catchは要らなくなる!

例外安全にするための技法

● Swap● 例外を投げうる部分と投げない部分を明確に分離

● 例外を投げる部分が成功したら例外を投げない部分で状態を変更

● ロールバック用のTry-Catchは要らなくなる!

(リソース管理用のは必要・・・)

例外安全にするための技法

● Swap● 自作クラスでも、

Copy-Constructableなクラスならswapを実装する。

例外安全にするための技法void SampleClass::swap(SampleClass &rhs){ //標準のswap関数や std::swap(data_, rhs.data_); //それ用のswap関数などで other_data_.swap(rhs.other_data_);}

例外安全にするための技法

● Swap● 自作クラスでも、

Copy-Constructableなクラスならswapを実装する。

● SampleClass

(SampleClass const &rhs);

例外安全にするための技法

● Swap● 自作クラスでも、

Copy-Constructableなクラスならswapを実装する。

● 実装すると・・・

例外安全にするための技法

● Swap● 自作クラスでも、

Copy-Constructableなクラスならswapを実装する。

● Assignableなクラスに出来る。● SampleClass &

operator=(SampleClass const &)

例外安全にするための技法SampleClass & SampleClass::operator= (SampleClass const &rhs){ //コピーコンストラクタ SampleClass tmp(rhs); //swap tmp.swap(*this); return *this;}

例外安全にするための技法SampleClass & SampleClass::operator= (SampleClass const &rhs){ //コピーコンストラクタ SampleClass tmp(rhs); //swap tmp.swap(*this); return *this;}

さらにまとめて書くと

例外安全にするための技法SampleClass & SampleClass::operator= (SampleClass const &rhs){ SampleClass(rhs).swap(*this); return *this;}

例外安全にするための技法SampleClass & SampleClass::operator= (SampleClass const &rhs){ SampleClass(rhs).swap(*this); return *this;}メンバ変数がAssignableであることを要求しない

例外安全にするための技法SampleClass & SampleClass::operator= (SampleClass const &rhs){ SampleClass(rhs).swap(*this); return *this;}

Copy And Swap

例外安全にするための技法

● RAII● 「リソースの確保は初期化である」

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //リソース管理のためにコードが複雑に}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //deleteも分散してしまった}

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; } //対応していない}

例外安全にするための技法

● RAII● 「リソースの確保は初期化である」● コンストラクタでリソースを確保● デストラクタでリソースを破棄

例外安全にするための技法

● RAII● 「リソースの確保は初期化である」● コンストラクタでリソースを確保● デストラクタでリソースを破棄

対応!!対応!!

例外安全にするための技法

● RAIIclass RAIIClass{ RAIIClass(Resource r) : r_(r) {} RAIIClass(Args as) : r_(CreateResource(as)) {}

~RAIIClass() { DisposeResource(r_); }private: Resource r_;};

例外安全にするための技法

● RAII● これをメモリ管理に使ったのがいわゆるスマートポインタ

例外安全にするための技法

● RAII● これをメモリ管理に使ったのがいわゆるスマートポインタ

● プログラマはリソース解放のめんどくささから解放される!

例外安全にするための技法

● RAII● これをメモリ管理に使ったのがいわゆるスマートポインタ

● プログラマはリソース解放のめんどくささから解放される!

● 例外安全とか考えなくても使うべき!

例外安全にするための技法

● RAII● これがどのように例外安全性の点で重宝されるのか?

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; }}

例外安全にするための技法

● RAII● Deleteのために例外をCatchしなくちゃいけなかった

例外安全にするための技法

● RAII● Deleteのために例外をCatchしなくちゃいけなかった

● でもRAIIなクラスを使うと、Deleteは自動化できる

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; throw; }}

これがこれが

例外安全にするための技法void SampleClass::ReadData(int n){ shared_array<T> tmp = data_; try { data_.reset(new T[n]); read_data(data_, n); } catch (...) { data_ = tmp; }}

こうなるこうなる

例外安全にするための技法

● RAII● どこで起きるかわからないような例外によって関数を抜けるときも、その時に自動でリソースが解放される!

● リソース解放のためのTry-Catchは要らなくなる!

例外安全にするための技法

● RAII● どこで起きるかわからないような例外によって関数を抜けるときも、その時に自動でリソースが解放される!

● リソース解放のためのTry-Catchは要らなくなる!(強い例外安全保証のためのは必要・・・)

例外安全にするための技法

● SwapとRAII

例外安全にするための技法

● SwapとRAII

1.Swapはロールバック用のTry-Catchを不要にする。

例外安全にするための技法

● SwapとRAII

1.Swapはロールバック用のTry-Catchを不要にする。

2.RAIIはリソース管理用のTry-Catchを不要にする。

例外安全にするための技法

● SwapとRAII

1.Swapはロールバック用のTry-Catchを不要にする。

2.RAIIはリソース管理用のTry-Catchを不要にする。

3.例外中立のために、例外が起きてもいじらないで伝える。

例外安全にするための技法

● SwapとRAII

合体合体

例外安全にするための技法void SampleClass::ReadData(int n){ shared_array<T> tmp(new T[n]); read_data(tmp_, n); std::swap(data_, tmp);}

例外安全にするための技法void SampleClass::ReadData(int n){ shared_array<T> tmp(new T[n]); read_data(tmp_, n); std::swap(data_, tmp);}

おわかりいただけただろうかおわかりいただけただろうか

例外安全にするための技法void SampleClass::ReadData(int n){ shared_array<T> tmp(new T[n]); read_data(tmp_, n); std::swap(data_, tmp);}

おわかりいただけただろうかおわかりいただけただろうかではもう一度・・・ではもう一度・・・

例外安全にするための技法void SampleClass::ReadData(int n){ T *tmp = data_; data_ = 0; try { data_ = new T[n]; read_data(data_, n); delete [] tmp; } catch(...) { delete [] data_; data_ = tmp; }}

例外安全にするための技法void SampleClass::ReadData(int n){ shared_array<T> tmp(new T[n]); read_data(tmp_, n); std::swap(data_, tmp);}

例外安全にするための技法

● SwapとRAII● 適切なコードを書けば、例外安全性のためにTry-Catchを書く必要はない。

例外安全にするための技法

● デストラクタで例外

例外安全にするための技法

● デストラクタで例外●禁止!

例外安全にするための技法

● デストラクタで例外●禁止!●配列を安全にできない● 例外が起きたときにスタックの巻き戻しを安全にできない

● 全ての例外安全の努力が水の泡!

5.まとめ

まとめ

● 例外安全なコードを書くには

まとめ

● 例外安全なコードを書くには● 例外を投げる部分と投げない部分を分離して、例外のある処理の成功を確認してから、状態を変更する。

● RAIIを使用してリソースの管理を自動化する

まとめ

● 今回の内容はほとんど全て「Exceptional C++」

(ハーブ・サッター著)

に書かれています● あと「C++ Coding Standard」も

おしまい。

Recommended