きままにブログスマートフォン用の表示で見るきままにブログプログラミングを主とした私のメモ帳です。寂しいのでコメントください笑この広告は、90日以上更新していないブログに表示しています。~RAIIのお話~C++概要C++の初心者向けに、クラスを利用したリソース管理及び特殊なメンバ関数であるコンストラクタ、デストラクタ、コピーコンストラクタ、ムーブコンストラクタと、デフォルトで定義されうるコピー代入演算子、ムーブ代入演算子について簡単な解説をしながらRAIIというC++で有名なイディオムの理解を深めていきます。ついでに、例外について簡単な解説をした後、例外安全について触れ、RAIIの利点を紹介します。第1回目は私の都合でムーブコンストラクタまでご紹介します。第2回目で後半を説明します。初心者にありがちなのですが、ポインタを使いこなそうとしています。残念ながら、実際はポインタをなるべく使わない方向に向けた方がいいのです。そのためにRAIIを活用していきましょう。コンストラクタとデストラクタ既にクラスの概要についてはご存じだとは思いますが、クラスにはコンストラクタという特殊なメンバ関数が存在します。これはクラスの初期化をするためのものですが、人によっては次のように書く人がいるみたいです。初期化処理}};これはこのクラスを使う人に負担を押しつける書き方なのでやってはいけません。(自作可能)を使用すればいい話です。ダメなのでしょうか?まずはじめに呼び出す時を考えます。これを呼ばないといけないこのようにコンストラクタを実行した後に無効な状態が出来てしまい、危険です。ユーザーは呼び忘れるかも知れません。初期化処理はコンストラクタに書きましょう。デストラクタですが、後で述べますが、通常はデストラクタはデフォルトのものを使用するべきです。デフォルト宣言します。default;};ただし、コンストラクタで何らかのリソース(メモリ、ファイル系、ソケット系、ハンドル系など)を獲得した場合は、デストラクタで解放しなければなりません。後で述べますが、通常はメンバに任せます。s;}関数func内でクラスSのインスタンスsがローカル変数として宣言されています。このため、関数func内ではスタックにクラスSのインスタンスを積みます。このとき変数sを初期化するためにSのコンストラクタが呼び出されます。この関数を抜けるときに(returnや例外のthrowあるいは関数の終端)デストラクタが呼び出され、スタックを破棄します。ローカル変数のスコープを抜けるときにデストラクタが呼び出されます。次のような場合もあります。sがローカル変数として宣言されています。このポインタは64bit環境では8byte(=64bit)の変数ですが、ポインタの指す先(実体)は確保できるどんなサイズでも構いません。実体はnew演算子によって生成され、スタック上にはなく、ヒープ上にあります。そのヒープ上を指し示すポインタをスタックに積んでいます。要らなくなったら明示的にdeleteしなければなりません。ポインタが関数を抜けても、そのポインタが使えなくなるだけで、実体は残ったママなのです。newすると実体が生成されますから、そのときにコンストラクタが呼ばれます。deleteするときにデストラクタが呼ばれて最後にメモリが解放されます。このような例を出してあれですが、初心者はnew/deleteを一切使わない、これに限ります。今回はこのnew/deleteを使った場合、即ちライブラリを作る側として話を進めますので、ついでに使い方も覚えてしまいましょう。new/deleteを使わないから、new/deleteを閉じ込めるにステップアップです。RAIIさてリソースについてですが――先ほどはnew/deleteを使ったメモリがそのリソースに相当しますが――先ほどのようにnewとdeleteが分かれていると管理が大変です。次の例を考えましょう。いうか誰がdeleteするの?}クラスSには何らかの条件判断を行うxxxメンバ関数が定義されているとします。この関数が成功したときには0を返し、そうでないときはそれ以外を返します。失敗したときはfuncはnullptrを返し、成功したときはnewしたポインタを返すとしましょう。この例だと1つだけですがこんなのが何個もあったら地獄絵図です。関数から抜けたときに、というのがポイントです。先ほどのデストラクタですね。これを利用してみましょう。s;};まずはこのようなSのラッパークラスを定義します。ラッパーとは、プログラミングでよく出てきますが、あるものを覆い被すようにして使いやすくするものです。上位レイヤーともいいます。ここではクラスPがクラスSのラッパーで有り、今回の主役です。デストラクタではsをdeleteします。従って、このクラスPのインスタンスはスコープを抜けたときに自動的にdeleteしてくれます。ここでもdeleteされてしまう!}このように、自動的にdeleteしてくれます。このようにコンストラクタでリソースを獲得し、デストラクタで後処理をする方法をRAIIといいます。ところがこのままでは問題があります。後で改善してみましょう。コピーコンストラクタコピーコンストラクタは、そのオブジェクトを渡してコピーするのが役目です。後で述べますが、通常はデフォルトにすべきです。次のクラスを考えます。y;};デフォルトでコピーコンストラクタが定義されているので、クラスのコピーが可能です。y)のコピーを逐一行ってくれます。メンバがコピー可能出ない場合はその限りではありません!従って、メンバにはコピー可能なものだけを並べる必要があります。コピーコンストラクタを自分で定義してみましょう。コピーコンストラクタも、いわばコンストラクタです。条件があって自分自身の参照を引数に取ります。メンバ{初期化値}で初期化します。ポインタのコピーは危険!さて本題です。ポインタがある場合です。ポインタをコピーするのは多くの場合誤りです。これをコピーするとどうなるか見てみましょう。(分かりやすくコピーコンストラクタを定義していますが、この場合はポインタ値はコピーできるので、デフォルトでコピー可能です。s;};これを使った関数funcを見てみましょう。pとp2のデストラクタが呼び出されるpとp2の中のsは同じポインタを指しています。2重のdeleteはやってはいけません。新しくSをnewして、中身をコピーするコピーを禁止する何らかの機構で2重deleteできないようにする参考までに標準ライブラリで言うと、1番目がstd::vectorのようなもの。2番目がstd::unique_ptrのようなもの。3番目がstd::shared_ptrのようなものです。自分が必要とするものを選びます。コピーコンストラクタでリソース確保ここでは、1番目の新しく確保してコピーを考えてみましょう。s;};とは言っても簡単で、コピーコンストラクタを改良しただけです。新しくnewするのですが、その際にコピー元となるit.sの実体である*it.sの参照を渡して、Sのコピーコンストラクタに任せる、というものです。クラスの責任はわけて、SのコピーはSに任せるのです。pとp2のデストラクタが呼び出されるが、大丈夫こうすることで先ほどの例である次のものが改善されます。新たにコピーを作って返す}と書けます。関数の戻り値では新しくコピーを作って返します。nullptrのようなものは返せなくなります。例外を使ってreturnではなくthrowする。Pがnullptrを持てるようにする。std::optionalのように無効値を持つクラスでラップする。このエラーがシステムの正常に稼働する範囲の状態の一つの時は例えばstd::optionalでラップしたり、nullptrを保持できるようにした方が良いでしょう。なく、起こったらユーザーに選択を迫らなければならなかったり、直前のロジックを辞めなければならなかったりするときは例外を使用するのが良い選択です。"初期化に失敗しました。内部のsはdeleteされる。新たにコピーを作って返す}これでめでたくSをラップしてメモリ管理をPに任せることが出来るようになりました。ちなみに何度も言いますが、通常はnew/deleteを使う必要がありません。今回はライブラリを作る側の話をしています。関数funcから受け取ってみましょう。std::endl;}}ポイントは4つです。例外の使い方ですが、tryブロックとcatchブロックに分かれます。tryのなかで例外がthrowされると、catchへ飛びます。このときローカル変数のデストラクタは全て呼び出されます。今回はfuncの中で例外が発生する可能性がありますが、まだpが構築されていないのでpのデストラクタは呼び出されません。itで受け取ることです。throwはどんなデータを渡しても良いのですが、お行儀の用意例外の書き方は、std::exceptionを継承した例外クラスを(必要なら)自分で定義してそれを渡します。これについてはまたいつか述べます。3つめはもしもfuncが成功したら、pは初期化されます。代入演算子ではなく、これはコピーコンストラクタが呼ばれます。(ここ感動するところです。)無駄を省くムーブコンストラクタ勘のいい人は気付くかも知れませんが、先ほど定義したクラスPには無駄があります。"初期化に失敗しました。中ではここでもSをnew}先ほども触れましたが、関数の戻り値にポインタや参照を指定しない場合、クラスをコピーして返します。};}のような感じになります。コピーされてしまうのです。ローカル変数のポインタや参照は絶対に返してはいけませんのでこれはこれで正しいプログラムなのですが、無駄がありますね。newは1回で良いはずなのに......。そんな理由で登場したのがムーブコンストラクタです。ムーブコンストラクタムーブコンストラクタは定義自体は簡単で、P&&を引数に取るコンストラクタです。s;};このコンストラクタが何か特別なことをしてくれるかというとそういうわけではありません。この中に、ムーブする処理を書かないといけないのです。