コンストラクタ,デストラクタでの例外処理

この記事は,C++プログラマであるかを見分ける10の質問 - Life like a clown の「コンストラクタ,デストラクタにおける例外処理についての戦略を述べよ」に対する回答的な記事になります.例外処理は通常においてもなかなか難しい処理の一つですが,コンストラクタ,デストラクタにおける例外処理をどうするかは,さらに頭を悩ませる問題となります.

コンストラクタ

コンストラクタにおいて例外を発生させる場合にはデストラクタが実行されないため,気をつける事は「(デストラクタで行われるはずであった)各種リソースの開放を例外を送出する前にきちんとを行う」になります.コンストラクタで例外を発生させるべきではない派の主張の論拠も大体ここに起因するようです.

比較的有名なサイトで「コンストラクタからの例外送出」が「禁じ手」として紹介されていることもあり、また、最近ではその内容を再編集した書籍が出版されたこともあって、コンストラクタから例外を送出すべきではないと考える人は多いようです。

その根拠となっているのは、コンストラクタから例外を送出した場合、デストラクタが呼ばれないためにリソースリークにつながるというものです。

[迷信] コンストラクタから例外を送出してはならない | 株式会社きじねこ

コンストラクタで例外が発生した場合,そのクラス自体のデストラクタは実行されませんが,(初期化の終了した)メンバ変数のデストラクタは実行されます.したがって,例えば new でメモリを確保するようなメンバ変数の場合は,スマート・ポインタで保持しておく事によってその部分のリソース開放が自動的に行われ,リソースの開放忘れのかなりの部分を防ぐ事ができます.

尚,メンバ変数の初期化中に送出された例外は以下(outer クラスのコンストラクタ部分)のように記述することによって catch できます (function-try-block).

#include <iostream>

class inner {
public:
   inner(int n) { throw n; } // ここで例外
};

class outer {
   inner in;
public:
   outer(int n) try : in(n) {
       std::cout << "~outer()" << std::endl;
   } catch ( int err ) {
       // 例外を捕まえる
       std::cout << "caught at outer's ctor" << std::endl;
   }

   ~outer() {
       std::cout << "~outer()" << std::endl;
   }
};
http://ml.tietew.jp/cppll/cppll/thread_articles/9439

デストラクタ

一方,デストラクタにおいては,「デストラクタでは例外を送出しない」と言う方針を取る事がほとんどです.

  • C++ の仕様は、コンストラクタやデストラクタが例外を投げてはいけないとは書いていないし、「注意事項」さえ守れば投げてもいい。実際、コンストラクタが例外を投げるのはそれほど悪いことではない。
  • 一方でデストラクタが例外を投げるようなコードは大抵良くない。何故かというと、上記「注意事項」の一つであるところの「例外によるスタック巻き戻し中に別の例外を投げてはいけない(いわゆるダブルフォルトの禁止)」を抵触しないようにプログラムを書くのが難しくなるから。

・・・(中略)・・・

  • これと比較して、デストラクタで例外を投げた場合、少なくともクラス実装だけでダブルフォルトを防ぐことは不可能だし、そもそもデストラクタで例外を投げさえしなければ、まず絶対にダブルフォルトは起こらない。もちろん、デストラクタが例外を投げたとしてもダブルフォルトが起こらないようにプログラムすることは不可能ではないのだが、デストラクタが例外を投げてしまうばっかりに、覚えなければならない概念(=ダブルフォルト)が増えてしまう。これは、投げることによる利益と比較すると大きな代償だ。だから、デストラクタは例外を投げちゃいけない。
http://diary.imou.to/~AoiMoe/2007.12/early.html

尚,デストラクタでは例外を送出しない事を明示化するために例外指定 throw() を書く事がありますが,例外指定は C++0x で deprecate になったようです.そのため,今後の互換性を考えると書くかどうかは悩むところです.

例外指定をdeprecateにする。例外指定は、現実では、まったく意味がなかった。例えば、テンプレートが絡むと、明示的に投げる可能性のある例外を指定するなどということは、不可能になってしまう。

ただし、いくつかのコンパイラでは、何も指定しない、throw()を、関数は例外を投げないという指定とみなして、そのような最適化をしている。そこで、noexceptというキーワードを、新たに導入して、「この関数は例外を投げないと保証できる」と宣言出来るようにした。

本の虫: post-Pittsburgh mailingの簡易レビュー

virtual デストラクタ

この記事は,C++プログラマであるかを見分ける10の質問 - Life like a clown の「virtual デストラクタの概要および使用上の戦略について述べよ」に対する回答的な記事です.

virtual デストラクタの役割は,派生クラスのデストラクタも適切に実行されるようにすると言うものです.virtual の付いていないデストラクタを持つクラスを継承して new でインスタンスを生成し,その継承したクラスへのポインタを基底クラスで保持してしまうと,delete の際に基底クラスのデストラクタのみが実行されてしまうので,派生クラスのデストラクタが実行されずリソースの開放漏れなどの問題が発生します.

virtual デストラクタに関しては,以下の記事で話題になったので一度流れを追ってみるのが良いと思います.

さて. virtual デストラクタに関する戦略ですが,簡単に言うと「インターフェース的な使われ方をするクラス(継承される可能性があり,かつ派生クラスのインスタンスを基底クラスのポインタで保持する可能性がある)の場合にはデストラクタに virtual を付ける」と言うものになります.

多くの場合、仮想デストラクタに関しては、

  • ポリモルフィズムをするための基底クラスには仮想デストラクタを持たせよう。特に仮想関数を持つクラスは必ず仮想デストラクタを持たせよう。
  • 継承を目的としていないクラス、または継承を目的としていてもそれがポリモルフィズムを目的としないのならば仮想デストラクタを宣言すべきではない。

となると思います。

ある程度経験を積んだC++プログラマは絶対にvirtualデストラクタのないクラスを継承しない? - 神様なんて信じない僕らのために

ここで,問題になるのが「あるクラスがインターフェース的な使われ方をするかどうかは分からない(インターフェース的な使い方をされないと言う保証ができない)」と言うものです.この辺は各人のクラス設計の問題にもなってくるのですが,「取りあえず全部に virtual を付けよう」派の主な論拠もこの辺りに起因しそうです.

個人的には,ある程度利用範囲の限られるクラス(あるプロジェクト内でのみ使われるとか)で今後どんな追加的な修正が入るか予想できないような場合には,「取りあえず全部のデストラクタに virtual を付けておく」と言う選択肢を取るのも良いと思います.ただし,これがいきすぎて「全てのクラスのデストラクタには virtual を付けなければいけない」のようなコーディング規約になってしまう(あるいは,そう盲目的に信じてしまう)のは良くないだろうと言う感想です.

コピーコンストラクタの扱い

この記事は,C++プログラマであるかを見分ける10の質問 - Life like a clown の「コピーコンストラクタおよび代入演算子の扱いにおける戦略について述べよ」に対する回答的な記事です.

C++ では,ユーザが明示的にコピーコンストラクタ,および代入演算子を定義しなかった場合,デフォルトのコピーコンストラクタ,および代入演算子が定義されます.この挙動は,各メンバ変数の値を単にコピーすると言うものなのでポインタなどが絡むと(ポインタ値がコピーされた後でコピー元のクラスで delete が発生するなどの)問題になる場合があります.そのため,コピーコンストラクタについては,

  1. 適切な動作を行うコピーコンストラクタ(および代入演算子)を明示的に定義する.
  2. 適切なコピーコンストラクタを定義できない場合は,コピー不可能にする.

と言う戦略を取ることになります.コピー禁止なクラスを作成する方法は,以下のように private にコピーコンストラクタ,および代入演算子の宣言だけを書いておきます.これによって,どこかでコピーが発生するようなコードが書かれた場合には,コンパイルエラーになってそれを防ぐ事ができます.

class Foo {
public:
    // 必要なメンバ関数などを定義する.
    // ...
private:
    // non-copyable
    Foo(const Foo&);
    Foo& operator=(const Foo&);
};

これをいちいち書くのは面倒と言う事で,Non-copyable クラス などを定義しておき,それを継承して使用すると言う事もしばしば行われます.

I/O のような何らかの資源(にアクセスするためのもの)をメンバ変数に持っている場合など,コピーコンストラクタの挙動をきちんと定義するのが難しいと言うケースはそれなりに存在するため,「コピー禁止にする」と言う選択肢を取る場面にも直々遭遇するのではないかなと思います.

ポインタの使用

この記事は,C++プログラマであるかを見分ける10の質問 - Life like a clown の「ポインタの使用方法について,メモリーリーク問題等と絡めながら戦略を述べよ」に対する回答的な記事です.

ポインタの使用に関する戦略ですが,経験上は以下の 2点辺りになるかと思います.

  • C++ は意外とポインタ(と言うよりは動的なメモリ確保)をユーザが明示的に使わずとも何とかなるケースが多いです.STL コンテナ等すでに用意されてあるライブラリの使用も考慮に入れ,本当にポインタが必要な場面かどうかを熟考しましょう.
  • ポインタが必要になった場合,「自力で後処理をする」と言う思想は危険なのでスマートポインタを使用するようにしましょう.

あんまりメモリリークの話絡めてないですね.

スマートポインタに関しては ポインタは用法用量を守って正しくお使いください - kikairoyaの日記 で上手く纏められていますので詳細はそちらを参照下さい.スマートポインタに関しては,現状では shared_ptr を使うと言う選択肢が一番多いだろうと予想されますので,「どの shared_ptr を使うか」と言う事について少し検討してみます.

まず,Boost C++ Libraries が使える(使う事を許されている)環境では boost::shared_ptr を使うのがベストでしょう.何らかの理由で Boost の使用が許されていない環境の場合は,コンパイラが比較的新しければ std::tr1::shared_ptr が存在するかと思いますのでそれを使用します.std::tr1::shared_ptr も存在しない場合は・・・自作になるでしょうか.ただし,shared_ptr および shared_array の実装は(その必要性から)多くの人が実装していますので,自分で作ると言う道を選ぶよりは既に誰かが実装した shared_ptr を持ってくる方が良いと思います.

現在の C++ に標準ライブラリとして唯一存在するスマートポインタである auto_ptr は扱いが難しいです.個人的な経験では,狭いスコープ内で new して一時変数に格納する(すぐに開放する)ようなケースでは auto_ptr でも良いかなと言う感じです.

纏めると以下のような形になります.

  1. Boost C++ Libraries が使えるなら boost::shared_ptr を使用する.
  2. TR1 が実装されている環境であれば std::tr1::shared_ptr を使用する.
  3. サードパーティ製の shared_ptr.h などを探して利用する.
  4. std::auto_ptr で代替する(限定的).

C++ でポインタが使われるケースは「コピーで発生するコストを嫌って」と言う話もしばしば耳にしますが,C++0x でムーブが定着するにつれてこの問題も少しずつ解消していく事が予想されます.その意味でも,そのコスト回避はどうしても必要なのかどうかはよく検討し,局所的な最適化はできるだけ抑えて,メモリ管理は標準ライブラリなどに任せるスタイルに移行しておく方が良いかなと個人的には思っています.

多重継承の概要

これは,C++プログラマであるかを見分ける10の質問 - Life like a clown の 「多重継承について概要を説明せよ」と言う質問に対する回答的な記事です.多重継承とは,その名の通り,以下のように 2 つ以上のクラスを継承する事を指します.

class Base1 {
public:
    virtual ~Base1() {}
    virtual void do_something1();
};

class Base2 {
public:
    virtual ~Base2() {}
    virtual void do_something2();
};

class Derived : public Base1, public Base2 {
    ...
};

一般的に多重継承になりやすいケースは,インターフェースや Mix-in と言った使い方をする場合でしょうか.Mix-in で一番よく使いそうな例だと Non-copyable Mixin 辺りかなと思います.「何らかのクラスを継承したクラスを作成したいんだけど non-copyable にしたい」と言ったケースでは多重継承はよく目にします.

多重継承で気をつけなければいけないのはダイヤモンド継承と呼ばれる問題です.あるクラスが継承した 2つ(以上)のクラスが,ある一つの基底クラスを継承していた場合,同じメンバ変数やメンバ関数(メソッド)が複数存在する事になるので,どの基底クラスのメンバー変数/関数を使うか曖昧さが残る事になります.これを仮想継承と言い,以下のように virtual キーワードを付与します.

class Base {
public:
    virtual ~Base() {}
    virtual void do_something();
    int foo;
};

class Derived1 : public virtual Base { // 仮想継承
public:
    virtual ~Derived1() {}
};

class Derived2 : public virtual Base { // 仮想継承
public:
    virtual ~Derived2() {}
};

class NestedDerived : public Derived1, public Derived2 {
    ...
};

多重継承は複雑になりがち,と言う理由で嫌う人もいるようです.多重継承を回避する方法としては,例えばインターフェースのような使い方だと,ポリシー・ベース(テンプレート・パラメータでインターフェース的なクラスを指定するようにする)でもある程度は代替できそうです.「多重継承だから禁止」のような方針はあまり良いとは思いませんが,まぁ各人が良いと思う形でクラス設計できれば良いのではないかと思います.

const 概要

これは,C++プログラマであるかを見分ける10の質問 - Life like a clown の「const の機能について概要を説明せよ」と言う質問に対する回答的な記事です.こんな時ですが,できるだけ平常更新を続けます.

C++ の const には大きく分けて 2つの役割があります.まず,基本的には const で宣言する事によって値が変更できなくなる(値を変更しようとするとコンパイルエラー)と言う役割があります.

const ing a = 100; // この値は変更不可

この機能を利用して,「ちょっとした一時変数であっても const にできる変数はできるだけ const 宣言し,想定外のエラーを減らそう(コンパイル時エラーで気付けるようにしよう)」と言うコーディングスタイルも提唱されていたりします.

さて,C++ のコードにおいて const をよく見かける機会は,「関数の引数」,「メンバ関数(メソッド)の宣言」の 2通りになります.

パラメータ

もっとも一般的なconstの使用方法としては、ポインタで指定されているオブジェクトや参照されているオブジェクトのデータの保護になります:

void func ( const MyObject * data ); // MyObjectはfuncの中では変更できません
void func ( const MyObject & data ); // MyObjectはfuncの中では変更できません

・・・(中略)・・・

メソッド

オブジェクトの内部のデータを変更しないインスタンスメソッドというのもよく作成されます。

・・・(中略)・・・

void MyClass::func ( MyOjbect & data ) const; // この関数はクラスのデータを変更することはできません

もしもconstのオブジェクトがあったとしたら、そのオブジェクトの中のメソッドのうち、呼び出せるのはconstの付いたメソッドだけです。理由としては、通常のメソッドが呼べてしまうとすると、そのオブジェクトのポインタをthisポインタとして渡す必要があります。しかし、そのクラスがconstが付いた”const MyClass”オブジェクトだったとすると、”MyClass *”ではなくて、”const MyClass *”しか得ることができませんので、constメソッドしか呼べないのです。

void MyClass::const_func() const;
void MyClass::func();
 
const MyClass object;
 
object.const_func(); // ok
object.func(); // constオブジェクトの非constクラスは呼ぶことができません

同様の理由で、constメソッドの中からそのオブジェクトのメソッドを呼ぶ場合には、他のconstメソッドしか呼ぶことはできません。

C + +のキーワード:const - cppreference.com

関数やクラスの設計で const 周りがおざなりの場合,const 付で渡されたが const メンバ関数が一つも定義されていないので const_cast を使うしかない,と言う状況がしばしば発生するので,この辺りの設計は注意して行う必要があります.

overload と override と hiding

これは, C++プログラマであるかを見分ける10の質問 - Life like a clown の 「overload と override と hiding の違いについて説明せよ」と言う質問に対する回答的な記事です.これらの差については,本の虫: C++におけるoverloadとoverrideとhiding 辺りでうまく纏まっているので引用して終わります.

同じ名前で、他のシグネチャの違う関数セットのことを、関数のオーバーロード(overload)という。

void f(int);
void f(double);

Derived classがBase classと同じvirtual関数を宣言しているとき、Derived classのvirtual関数は、Base classの同virtual関数を、オーバーライド(override)しているという。

struct Base
{
    virtual void f() {}
};

struct Derived : Base
{
    virtual void f() {} // override
};

Derived classのメンバー名に対し、同名のBase classのメンバーがある場合、Derived classのメンバーは、Base classのメンバーを隠している(hiding)という。

struct Base
{
    void f(int);
    int value;
};

struct Derived : Base
{
    void f(double);
    double value;
};

int main()
{
    Derived d;

    d.f(0); // void Derived::f(dobule)を呼ぶ
    d.value; // double Derived::valueである
}

結局これは、Derived classスコープのメンバーである名前が、Base classスコープのメンバーの名前を隠しているに過ぎない。同じことは、クラス以外のスコープでも起こる。

本の虫: C++におけるoverloadとoverrideとhiding

overload と override の違いは様々な所でよく言われるので分かると思いますが,C++ では*1これらの他に hiding と言うものが存在します.C++ において,メンバ関数を override するためには基底クラスの該当メンバ関数が virtual 宣言されていなくてはなりませんが,virtual 宣言されていなくても一見すると override されているような挙動を示します.これが hiding になります.

override と hiding の違いは,基底クラスのポインタで操作を行うような場合に問題になってきます.virtual 宣言のなされたメンバ関数は基底クラスのポインタから該当のメンバ関数を呼び出しても継承先クラスの override したメンバ関数の挙動を示しますが,virtual 宣言されていないメンバ関数の場合は基底クラスの挙動を示します.このような違いがあるので,問題となっているクラスがインターフェース的な使われた方をする場合は特に,注意して使用する必要があります.

*1:別に C++ 以外のプログラミング言語でもありそうですが.

C++ のキャスト

これは,C++プログラマであるかを見分ける10の質問 - Life like a clown の 「*_cast およびCスタイルのキャストそれぞれについて概要を説明せよ」と言う質問に対する回答的な記事です.この辺りからしばらくただの説明的なものになるのでざっと書いていきます.

C++ のキャストに関しては,http://www.s34.co.jp/cpptechdoc/article/newcast/ がコンパクトに纏まっています.

static_castはexprの型からtypeへの暗黙の型変換、あるいはtypeからexprへの暗黙の型変換が存在する場合にだけキャストします。キャスト不可能であればコンパイルエラーとなります。

reinterpret_castはtype(expr)が許されるなら、exprをtypeに単にキャストします。reinterpret_castは単なる型変更であり、たとえ派生関係があったとしてもポインタのアドレス自体はキャスト前と変わりません。その意味でreinterpret_castは非常に危険なキャストといえるでしょう。

const_castはconstおよびvolatile修飾子を無効にするだけのキャストを行ないます。そのほかのときはコンパイルエラーとします。

dynamic_castは基底クラスへのポインタ(or 参照)から派生クラスへのポインタ(or 参照)への型保証キャストを行ないます。上記3種のキャストはコンパイル時にキャストしますが、dynamic_castは実行時に型の検査が行なわれ、変換不可能であれば0を返します。dynamic_castにより、従来のキャストでは不可能であった クロス・キャスト、そして抽象基底クラスからのダウン・キャストが可能になりました。

http://www.s34.co.jp/cpptechdoc/article/newcast/

キャストの中で一番混乱する部分は static_cast と reinterpret_cast の違いですが,reinterpret_cast は値を保ったまま型情報のみを変更するのに対して,static_cast は必要に応じて値自体も変更します.

C スタイルのキャストは,C++ の static_cast, reinterpret_cast, const_cast の 3種類を纏めたものになります.

C形式のキャストは、

  1. const_cast
  2. static_cast
  3. static_castとconst_cast
  4. reinterpret_cast
  5. reinterpret_castとconst_cast

このような順番と組み合わせで表現できる。これらのキャストを上から順番に試していき、キャストが行えるところで、そのキャストを行う。

本の虫: 邪悪なC形式のキャストにしかできないこと

iterator の役割

これは,C++プログラマであるかを見分ける10の質問 - Life like a clown の 「iterator の役割について説明せよ」 と言う質問に対する回答的な記事です.iterator の詳細については Iterator library - cppreference.com 辺りを参照してもらうとして,ここでは「なぜ iterator と言うものが存在しているのか?」と言う iterator の役割の観点から記述します.

iterator の役割については,Boost.勉強会 #3 での id:Cryolite (@Cryolite) の発表資料(の前半部分)が分かりやすいので引用します.


iterator の役割は「データ構造とアルゴリズムの橋渡し(インターフェース)」になります.C++ は,iterator と言うインターフェースが定義されている事によって, データ構造とアルゴリズムをうまく分離する事に成功しています.

ユーザは,データ構造であればそのデータ構造を走査する iterator を定義する事によって,アルゴリズムであれば各 iterator に対する処理を記述することによって,既に存在する他の(iterator ベースの)アルゴリズムやデータ構造を再利用することができます.

この質問を一番最初に持ってきた理由は,iterator の役割を認識していない人と C++ のコードを共有した場合,「STL の <algorithm> を使ったら『お前の書くコードは可読性が低い.俺にも初心者にも分かるように書け』と言われた.な,何を(ry」と言うホラーな状況が割と発生するので,事前にその状況を回避するためだったりします.

std::vector<std::string> v;
v.push_back("hello, world!");
v.push_back("foo");
...

// index で各要素にアクセス
for (std::size_t i = 0; i < v.size(); ++i) {
    do_something(v[i]);
}

// iterator で各要素にアクセス
for (std::vector<std::string>::iterator pos = v.begin(); pos != v.end(); ++post) {
    do_something(*pos);
}

実際,上記のような(サンプルコードとしてよく見かける)コードだけだと iterator の存在意義がイマイチよく分からないと言う人も多そうなので,iterator の役割と言うか存在意義について問うてみるのは大切かなと思います.もっと直球に「<algorithm> 使ってる?」とかでも良いかもしれませんが.

iterator に関しては「begin/end をペアで書くのがめんどくさい」と言う切実な問題によって,これに変わるインターフェースとして range が議論され,いくつかのライブラリが実装されています.将来的には,一番外側のインターフェースとしては,これに取って代わられるかもしれません.

C++プログラマであるかを見分ける10の質問

「優れたPerlプログラマを見分ける27の質問」の日本語訳 - Islands in the byte stream, Javaプログラマであるかを見分ける10の質問 - やさしいデスマーチ を見ながら C++ だとこれくらいかなぁと取りあえず作ってみました.前半は単純な機能の説明,後半は実際にどのように使って(使い分けて)いくかについての質問になっています.選択基準としては,特に後半部分は,自分が観測できている C++ 界隈で論争になったトピックを中心に取り上げています.

  1. iterator の役割について説明せよ
  2. *_cast およびCスタイルのキャストそれぞれについて概要を説明せよ
  3. overload と override と hiding の違いについて説明せよ
  4. const の機能について概要を説明せよ
  5. 多重継承について概要を説明せよ
  6. ポインタの使用方法について,メモリーリーク問題等と絡めながら戦略を述べよ
  7. コピーコンストラクタおよび代入演算子の扱いにおける戦略について述べよ
  8. virtual デストラクタの概要および使用上の戦略について述べよ
  9. コンストラクタ,デストラクタにおける例外処理についての戦略を述べよ
  10. 抽象クラスとテンプレートクラスの使い分けについてインターフェースと言う観点から述べよ

主観がかなりはいっていますので,これ外してこっちのトピックの方がいいんじゃね?とか有りましたら,どこかで適当に捕捉して下さい.私自身もきちんとした言葉で記述できるかどうかは怪しいので,そのうち一度自分の言葉で纏めてみようかと思います.