C++のコンストラクタとデストラクタ

フォーラム(掲示板)ルール
フォーラム(掲示板)ルールはこちら  ※コードを貼り付ける場合は [code][/code] で囲って下さい。詳しくはこちら
atori
記事: 43
登録日時: 8年前

C++のコンストラクタとデストラクタ

#1

投稿記事 by atori » 6年前

お久しぶりです。覚えている方がいらっしゃるならですが。

今回はC++の初歩的な質問をさせていただきたく、トピックを立てました。

最近、C++でコードを書く際、デストラクタに何も記述しないことが多くなりました。
昔はデストラクタに色んな終了処理(DirectXだったらRelease関数呼んだり)していたのですが、
それも自分で作った終了処理用関数(Finalize()とします)を呼ぶことでやるようにしています。

そうしている内に、ふと「せっかくC++使ってるのにデストラクタ使わないのおかしくね?」と今日思いました。

デストラクタに何も書かなくなった経緯なのですが、
デストラクタで終了処理やるなら、コンストラクタで初期化したほうがわかりやすいだろう→
初期化するには様々な引数が必要になる事が多々ある→
コンストラクタで初期化しているクラスを別のクラスにメンバとして持つだけでは、当然エラーになる→
エラー回避の為にコンストラクタの引数がどんどん増える→
ので、初期化は自分で関数(Initialize()とします)を作る→
Initialize()で初期化したなら、Finalize()を作るべき


という流れです。で、つい最近まで、Finalize()とデストラクタに全く同じコードを記述していました。

コード:

 
// SafeRelease()はテンプレート関数

class Hoge
{
private:
        デバイスポインタ   pDevice;

public:
        Hoge():pDevice(nullptr){}
        ~Hoge(){SafeRelease(pDevice);}  // ここ同じ

        Initialize( なんか引数);
        Finalize(){SafeRelease(pDevice);} // ここ同じ
}
が、「確かにFinalize()呼び忘れのためにデストラクタに書いておいてもいいけど、これは無駄な処理だ」
と思い、今では、デストラクタには何も書かなくなりました。


皆さん、C++でコードが書く際、初期化と終了処理をどうしていますか?
自分の中では、コンストラクタは引数なしで、メンバの初期値を終了する(ほとんどNULLか0)場、デストラクタではなにもしない。
としていますが、これってどうなんでしょうか?

C++初めて3年経ち、いまさら聞けない内容なのですが、聞かぬは一生の恥だと思い質問しました。
よろしくお願いします。
Done is better than perfect.(Mark Elliot Zuckerberg)

アバター
せんちゃ
記事: 50
登録日時: 10年前
住所: 江別市東野幌町
連絡を取る:

Re: C++のコンストラクタとデストラクタ

#2

投稿記事 by せんちゃ » 6年前

自分はコンストラクタにはフィールドの初期化処理のみを記述して引数は基本渡さない
という文化で育ったのでコンストラクタには初期化処理のみ書いてあとは
Initialize,finalizeのような関数を頭とお尻に呼ぶようにしています。
しかしこれはinitializeされたらかならずfinalize を呼ばなければいけない、というルールを制定しないとメモリリークの原因になり得ます。
つまりはどこかしらでassertなどで例えばインスタンスはNULLになってるか?みたいなチェックを行わなければいけません。
これは初期化も同じです。
initializeメソッドが存在するということは、initialize されるまで機能は利用してはいけない
というルールを設ける必要があります。
Initialize が呼ばれず、finalize のほうが先に呼ばれた場合どうなるのか、とか
Initialize されたはいいがfinalize が呼ばれなかった場合どうなるのか、とかです。
こういったヒューマンエラーはちゃんと正しい使い方をしろというエラーメッセージを呼び出してあげる必要があります。
そのためこういった実装方法になるとインスタンス側の実装は必然的にassertだらけになります。


入り口と出口がガッチリ作られていてヒューマンエラーが直ぐに検知できるのであれば、コンストラクタ、デストラクタであろうとinitialize ~finalize であろうと特に違いはない気がしています
ヽ(*゚д゚)ノ カイバー

アバター
h2so5
副管理人
記事: 2212
登録日時: 9年前
住所: 東京
連絡を取る:

Re: C++のコンストラクタとデストラクタ

#3

投稿記事 by h2so5 » 6年前

atori さんが書きました: 初期化するには様々な引数が必要になる事が多々ある→
コンストラクタで初期化しているクラスを別のクラスにメンバとして持つだけでは、当然エラーになる→
エラー回避の為にコンストラクタの引数がどんどん増える→
このあたりの状況が具体的によく分からないので、くわしく説明していただけると助かります。

アバター
nullptr
記事: 239
登録日時: 8年前

Re: C++のコンストラクタとデストラクタ

#4

投稿記事 by nullptr » 6年前

とりあえず、話に入る前に

コード:

 
// SafeRelease()はテンプレート関数
 
class Hoge
{
private:
        デバイスポインタ   pDevice;
 
public:
        Hoge():pDevice(nullptr){}
        ~Hoge(){SafeRelease(pDevice);}  // ここ同じ
 
        Initialize( なんか引数);
        Finalize(){SafeRelease(pDevice);} // ここ同じ
}

コード:

 
// SafeRelease()はテンプレート関数
 
class Hoge
{
private:
        デバイスポインタ   pDevice;
 
public:
        Hoge():pDevice(nullptr){}
        ~Hoge(){this->Finalize();}  // ここ同じ
 
        Initialize( なんか引数);
        Finalize(){SafeRelease(pDevice);} // ここ同じ
}
とすればコードの重複は避けられますよね…????


本題ですが、ここから話すのはこれが正しい!という話ではないのでそれだけ意識して下さい。設計や環境、ルールによって話は変わりますから。あくまで私の場合です。

私が個人で開発しているプロジェクト内では、「インスタンスが生成された時点で(コンストラクタを抜けた時点で)そのオブジェクトは使用可能である」という前提を作るように設計しています。
そして、デストラクタが呼ばれたら必ず終了処理が走る、という前提であります。

インスタンスが存在している時点で、使用可能であるという前提は逐次チェックなどを省くことが出来ます。
生成に失敗した場合は、コンストラクタから例外を投げます。仮生成をするとしても、必ず使用可能であり、インスタンスの意味を満たすように設計します。

例ですが、テキストファイルを表現するクラスがあったとします。その場合、私はそのクラスにload関数を設けたりしません。
そのクラスがインスタンス化される時点でデータは揃っていて、インスタンス化された後はすぐに操作が可能な万全な状態であるようにします。
「テキストファイルが存在しない状態」を、テキストファイルクラスには持たせません。


しかし、当然そういった状態を表現したい場合や、遅延初期化をしたい場合もあるかと思います。しかしそのためにこの前提を崩すのは好みません。
私は「使用可能でない状態」を表すクラスを使います。例としては、boost.optionalです。実際は自分用にカスタムした物を使いますが、boost.optionalが元です。

boost.optionalは値の再束縛を可能にします。
参考文献
参考文献

つまり、まだ使用しない時点では無効状態を表す状態にしておき、構築したいタイミング(initializeと同じ)で構築し、破棄したいタイミングまたは寿命のタイミングで破棄されます(デストラクタを終了条件にしていれば、finalizeされる必要はない)。
ちなみにメモリの動的確保は起きません。コストを同じくしてオブジェクトの構築タイミングを制御できます。同時に、無効状態を統一的に表現できるため、「このクラスならisEmptyを呼ぶ」とか「この変数がNULLか」などといったそれぞれの無効値の確認方法を意識する必要もありません。

オブジェクトに「無効である状態」を持たせない設計にする事で、オブジェクトのインターフェイスはシンプルになり、人為的ミスも減らすことが出来ると考えます。


私が避けようとしているのは単なる初期化と終了の保証されない状態ではなく、その方法の分散でもあります。終了処理がReleaseであったり、Finalizeであったり、もしかしたらEndという関数だったりいろいろ可能性があります。
全てデストラクタで扱えれば覚えることも書くことも減ります。デストラクタは普通の関数とは違い寿命の時点で呼ばれる保証もあります(ただしその辺の保証がないなど特定の自立処理系などは別の話)。


最も、せんちゃさんのおっしゃるとおり、
入り口と出口がガッチリ作られていてヒューマンエラーが直ぐに検知できるのであれば、コンストラクタ、デストラクタであろうとinitialize ~finalize であろうと特に違いはない気がしています
というのが答えだとも思います。
 
 
✜ で C ご ✜
: す + 注 :
¦ か + 文 ¦
?
Is the は :
order C++? ✜
     糸冬   
  ――――――――
  制作・著作 NHK
 
 

アバター
softya(ソフト屋)
副管理人
記事: 11677
登録日時: 10年前
住所: 東海地方
連絡を取る:

Re: C++のコンストラクタとデストラクタ

#5

投稿記事 by softya(ソフト屋) » 6年前

大枠のところは出ているので、細かい所で便利なデストラクタの使い方など。

●資源のロックなどでデストラクタを使うと便利。
スレッドなどの処理で資源をセマフォ・ロックする場面もあるかと思います。
これいちいち、ロック・アンロックと書いていると条件でreturn;とかで抜ける処理があると煩雑になり書き忘れる場合も出てきます。

MFCですが、こんなクラスを使っています。

コード:

class CSyncSLock {
private:
	CSingleLock singleLock;
public:
	CSyncSLock(CSyncObject* pObject) : singleLock(pObject,TRUE) {};	//インスタンス生成と同時にロック
	~CSyncSLock() { Unlock(); };
public:
	void Unlock() {
		singleLock.Unlock();
	}
};
使い方はこんな感じ。ロックしたくなったら、インスタンスを生成するだけです。

コード:

CSyncSLock sync_sLock(&m_mutex);
関数を抜けるとインスタンスが消滅してデストラクタでアンロックされるので、ロックの解除を意識する必要がありません。
これで面倒なことを考える場面が減ります。つまりバグも減ります。
by softya(ソフト屋) 方針:私は仕組み・考え方を理解して欲しいので直接的なコードを回答することはまれですので、すぐコードがほしい方はその旨をご明記下さい。私以外の方と交代したいと思います(代わりの方がいる保証は出来かねます)。

atori
記事: 43
登録日時: 8年前

Re: C++のコンストラクタとデストラクタ

#6

投稿記事 by atori » 6年前

せんちゃ さんが書きました:こういったヒューマンエラーはちゃんと正しい使い方をしろというエラーメッセージを呼び出してあげる必要があります。
そのためこういった実装方法になるとインスタンス側の実装は必然的にassertだらけになります。
確かに現在作っているクラスはassertだらけになっています。

h2so5 さんが書きました:
atori さんが書きました: 初期化するには様々な引数が必要になる事が多々ある→
コンストラクタで初期化しているクラスを別のクラスにメンバとして持つだけでは、当然エラーになる→
エラー回避の為にコンストラクタの引数がどんどん増える→
このあたりの状況が具体的によく分からないので、くわしく説明していただけると助かります。
コードで書きます。

コード:

// コンストラクタで引数を必要とするクラスA
class A{
        A(Hoge hoge);
        ~A();
};

// クラスAをメンバに持つクラスB
class B{
private:
        A    a;
public:
        B(Hoge hoge):a(hoge); // Bのコンストラクタに引数が必要になる
        ~B();
};
こういうことです。
説明足らずでした。すみません。

新々月 さんが書きました:とりあえず、話に入る前に

コード:

 
// SafeRelease()はテンプレート関数
 
class Hoge
{
private:
        デバイスポインタ   pDevice;
 
public:
        Hoge():pDevice(nullptr){}
        ~Hoge(){SafeRelease(pDevice);}  // ここ同じ
 
        Initialize( なんか引数);
        Finalize(){SafeRelease(pDevice);} // ここ同じ
}

コード:

 
// SafeRelease()はテンプレート関数
 
class Hoge
{
private:
        デバイスポインタ   pDevice;
 
public:
        Hoge():pDevice(nullptr){}
        ~Hoge(){this->Finalize();}  // ここ同じ
 
        Initialize( なんか引数);
        Finalize(){SafeRelease(pDevice);} // ここ同じ
}
とすればコードの重複は避けられますよね…????
あ・・・そうですね。その通りです。
私が言いたかったのは、Finalizeでもデストラクタでも同じ処理が走ってしまう(デストラクタでReleaseはされないけど)
のが無駄だと感じるということです。

softya(ソフト屋) さんが書きました: 大枠のところは出ているので、細かい所で便利なデストラクタの使い方など。

●資源のロックなどでデストラクタを使うと便利。
スレッドなどの処理で資源をセマフォ・ロックする場面もあるかと思います。
これいちいち、ロック・アンロックと書いていると条件でreturn;とかで抜ける処理があると煩雑になり書き忘れる場合も出てきます。
やはり必ず呼ばれるという保証があるというのが利点なのですね。
それを有効活用していきたいと思います。


せんちゃさんや新々月さんの書かれたことを見ていると、デストラクタは有効活用したほうがいい気がしてきました。
ヒューマンエラーによるバグをなるべく取り除くことが大事だということも改めて知ることが出来ました。
皆さん有難うございました。
Done is better than perfect.(Mark Elliot Zuckerberg)

YuO
記事: 942
登録日時: 9年前
住所: 東京都世田谷区

Re: C++のコンストラクタとデストラクタ

#7

投稿記事 by YuO » 6年前

解決となっているけれども,誰も例外安全について書いていないので……。

C++を使う以上例外から離れることはできません。
終了処理を別関数にしてしまうと,C++にはfinallyがないので,そこかしこにtry - catch書いて,catchの中と外に終了処理関数の呼び出しを書くことになってしまいます。
なので,終了処理が必要なのであれば,私はデストラクタを使います。
オフトピック
本物の C++er はデストラクタを書かない - 野良C++erの雑記帳なんてのもありますが,これもRAIIの話なので終了処理関数ではなかったりします。
例外安全のために,デストラクタでの終了処理をさらに小さなクラスに委譲する,という話です (例示されているのはunique_ptr)。

閉鎖

“C言語何でも質問掲示板” へ戻る