ポインタの有効性

[戻る]
亀山 2008/12/01(月) 09:39:17
class CItem {…};

CItem* m_pItem = new CItem;
return m_pItem;

このあとに、returnされたアドレスだけを使って
pの指す先の実体がdeleteされずに残っているかを
任意のタイミングで確認する方法はあるのでしょうか?

それとも、CTypedPtrListのようなものを使って、
ポインタを一括管理するようなクラスを用意するべきなのでしょうか?

tetrapod 2008/12/01(月) 10:27:25
生ポインタをそのまま return するってのは現代 C++ 的には時代遅れで、
shared_ptr とかに wrap するのがいいんぢゃないかな。

> それとも、CTypedPtrListのようなものを使って、
> ポインタを一括管理するようなクラスを用意するべきなのでしょうか?
一括管理という用語の意味が俺には通じていないんだが、目的次第。
delete し忘れを中央集権方式デバッグ補助で見つけたいとかそういう意図?
俺ならたぶんやらない。
shared_ptr とかにて代用して 生 delete を排除する方向に持ってくとおもう。
delete をそもそも書く必要がなくなるので、書き忘れもなくなる。

aetos 2008/12/01(月) 10:52:50
> 任意のタイミングで確認する方法はあるのでしょうか?

あるかないかと言えば、ないです。

bun 2008/12/01(月) 11:02:16
確実に確認する方法というのは存在しないんじゃないかなぁ。

運用方法を想定し、結果的にというならいくつかありそう。
もっとも簡単なところでは、deleteしたら必ずNULLを代入する約束にしておけば、NULL
チェックだけで済みます。

また、ご自身もおっしゃっているように、他のクラスにポインタの管理を任せてしまう
のも一つの手です。
実際、私はテンプレート型のリストクラスにポインタを一括管理させており、最近はほ
とんどdelete文を書いていません(リストクラスのデストラクタにて自動delete)。

いずれも運用によるものですから、運用方法を守らない人がいたらダメですね。

仲澤@失業者 2008/12/01(月) 11:03:55
> このあとに、returnされたアドレスだけを使って
> pの指す先の実体がdeleteされずに残っているかを
> 任意のタイミングで確認する方法はあるのでしょうか?

単なるアドレス数値の指し示す先が有効なオブジェクト
であるかどうかを調べる簡単な手段はありません。

この場合「delete オペレータを公開しない」など、
不用意にdeleteされないことを目指すべきかもしれません。
そうすればポインタは常に有効であることを「ある程度」
保障できます。

亀山 2008/12/01(月) 11:25:18
tetrapodさん、aetosさん、bunさん、仲澤@失業者さん、
解説ありがとうございます。

ツリー構造の情報を内部で持っているのですが、
Windowsのツリービューのアイテムなんかは
_TREEITEMという内部構造体のポインタをHTREEITEMとして返しているようです。

これと同じような感じで、内部でnewで確保したオブジェクトのポインタを
戻り値として返しているのですが、それを再度引数に取る別関数の中で
「指定されたアドレスはちゃんと内部にオブジェクトが存在しているものか」
をチェックしておきたいのです。

もちろん、内部ではポインタを見失うような構造にはなっていないので、
ツリーのように先頭から一つずつ階層を行き来して探していけば
判断自体はできるのですが、関数が呼ばれるたびに探し直していくことになり、
オブジェクトの数や呼ばれる回数が増えてくると応答時間も無視できなくなるため、
渡されたポインタを見るだけで「存在するオブジェクトのアドレスか」を
判断できないものだろうかと思って質問させていただきました。

やはりアドレスを見るだけでは断定できないのですね。
ということは、ツリービューもSetItemText()などの際には
HTREEITEMの有効性を内部で探索しているということでしょうか。

> deleteしたら必ずNULLを代入する約束にしておけば

ちなみに、この方法の場合、どこにNULLを入れるのでしょうか?
内部からは、deleteするオブジェクトのポインタ自体は
誰がどこに持ち続けているかはわからないのではと思うのですが。

bun 2008/12/01(月) 11:44:20
> ちなみに、この方法の場合、どこにNULLを入れるのでしょうか?
いまいち、何がしたいのかが分からないのですが、

> CItem* m_pItem = new CItem;
> return m_pItem;
における、m_pItemは m_ がついていることからして、何らかの
クラスのメンバを想定しているのかな?

だとしたら、
> return m_pItem;
の戻り値を、CItem* では なく、CItem*& にすれば、外側から内部の
値を変更できます。
が、あまりお勧めできません。
クラスによる隠蔽効果が薄れますから。

メンバであるなら、deleteは特定のメンバ関数かデストラクタに任せ
た方が良いと思います。

たいちう 2008/12/01(月) 11:54:08
> これと同じような感じで、内部でnewで確保したオブジェクトのポインタを
> 戻り値として返しているのですが、それを再度引数に取る別関数の中で
> 「指定されたアドレスはちゃんと内部にオブジェクトが存在しているものか」
> をチェックしておきたいのです。

ポインタを外部が持っている間に、内部でポインタを無効にする可能性があるという
ことでしょうか?外部がポインタを長期間持たないようにするのがベストかと。

外部が何らかの操作でポインタを必要とするときに、内部の関数を呼び出して
ポインタを取得し、操作終了後は外部はその値を管理しない。
常に最新の情報を内部から取得するわけです。


以上の方針が基本だと思いますが、効率面でどうしてもというならば、
トリックを考えましょう。1つ思いついたものを書きますが、亀山さんの
問題の解決方法としてふさわしいかは分かりません。


class Piyo;

class PiyoWrapper {
    Piyo *piyo_;
    bool isValid_;
public:
    PiyoWrapper() {
        piyo_ = new Piyo();
        isValid_ = true;
    }
    void DeletePiyo() {
        delete piyo_;
        piyo_ = NULL;
        isValid_ = false;
    }
    ~PiyoWrapper() {
        delete piyo_;
    }
};

class Hoge {
    std::vector<PiyoWrapper> wrapper_;
};

内部クラスがHogeです。
内部クラスがPiyoを作るときに、PiyoWrapperとして作り、
Hogeの寿命を通してPiyoWrapperのインスタンスは残します。
各クラスのアクセサ等は適当に追加・変更してください。

外部はPiyoWrapperへのポインタを内部からもらいます。
Hogeの寿命を通してPiyoWrapperが有効であることは保証されていて、
isValid_でPiyoの有効性を判断します。

aetos 2008/12/01(月) 12:24:58
> Windowsのツリービューのアイテムなんかは
> _TREEITEMという内部構造体のポインタをHTREEITEMとして返しているようです。

ここには疑問を挟む余地があります。
commctrl.h の中で
 typedef struct _TREEITEM *HTREEITEM;
と定義されていることを指しているのだと思いますが、実際に Windows OS のソースコー
ド中でも、HTREEITEM が _TREEITEM 構造体へのポインタであることは保証されません。
ひょっとしたら内部的には、ポインタではなく、単なる整数値かもしれません。

ではなぜ commctrl.h ではこのように定義されているのかと言うと、推測ですが、こうし
ておけば HTREEITEM 型の値を HWND 型に代入するようなコードを書いた場合、コンパイ
ルエラーにすることができるというメリットがあるためかもしれません。
 typedef DWORD_PTR HWND;
 typedef DWORD_PTR HTREEITEM;
では、エラーにすることができませんからね。

> これと同じような感じで、内部でnewで確保したオブジェクトのポインタを
> 戻り値として返しているのですが、それを再度引数に取る別関数の中で
> 「指定されたアドレスはちゃんと内部にオブジェクトが存在しているものか」
> をチェックしておきたいのです。

関数を作る側と、それを使う側には、約束事が存在します。
引数にポインタを取る関数であれば、そこに delete 済みのポインタや、ポインタと偽っ
た整数値を渡さないというのは使う側の約束であり、有効なポインタを渡されたからには
きちっと動作するというのは作る側の約束です。
基本的には、その約束は破った方がペナルティを負うべきなので、delete 済みのポイン
タを渡した結果、アプリが落ちてしまっても、それは呼び出し側が悪いので、関数側でフ
ォローすべきことではないと思います。

tetrapod 2008/12/01(月) 13:40:37
わき道雑談
> ポインタを外部が持っている間に、内部でポインタを無効にする可能性がある
CWnd::GetDlgItem, GetWindow, その他がこれだなぁ
いつ無効になるのか MFC ソースをちまちま追っかけたなぁ (遠い目)

ちなみに commctrl.h を見ると
#define TVI_ROOT ((HTREEITEM)0xFFFF0000)
とかあるのでそもそも原理的にチェックできない、と判断しますな

bun 2008/12/01(月) 14:39:19
読み返してみて、何となく分かった気がします。

クラス内でnewしたオブジェクトのポインタを外部に引き渡してしまうと、
外部でdeleteされないか心配ということでしょうか?
そういうことなら気持ちは分かりますが、どうしようもない気がします。

だって、以下のようなコードもありですから、
int* CTest::Func()
{
  return &m_i;
}

これで、戻り値 delete されたら、その時点でオジャンです。
こんなの責任もてません。

そんなわけで、私も aetos さんの意見に賛成です。
> 基本的には、その約束は破った方がペナルティを負うべきなので、...

亀山 2008/12/01(月) 15:23:34
[[解決]]
bunさん、たいちうさん、aetosさん、tetrapodさん、解説ありがとうございます。

> クラス内でnewしたオブジェクトのポインタを外部に引き渡してしまうと、
> 外部でdeleteされないか心配ということでしょうか?

それもありますが、今回の件は
「クラス内ですでにdeleteしたオブジェクトのポインタを外部から渡されたことを、
そのポインタだけで判断できないか」ということでした。

Windowsのツリービュー(CTreeCtrl)の場合、

HTREEITEM hItemParent = m_tree.InsertItem(_T("Parent"));
HTREEITEM hItemChild  = m_tree.InsertItem(_T("Child"), hItemParent);
m_tree.DeleteItem(hItemParent);
m_tree.SetItemText(hItemParent, _T("SetItemText"));
m_tree.SetItemText(hItemChild,  _T("SetItemText"));

のSetItemText()で親も子もちゃんとFALSEが返ってきます。
ハンドルにはInsertItem()の戻り値が入ったままです(すでにゴミ)。

これと同じような挙動にしておきたいなと思い、
アイテムを更新したり削除したり子を追加したりするような関数すべての内部で
「念のためまず今の階層を行き来して存在自体をチェック」していたのですが、
応答時間などの問題が出てきたため、
渡されたアドレス値そのものから一発判定できないかなと思いました。

たしかに、こんな呼びかたは実際の使用では想定外ですし、
そこまで考慮してやるものでもないのですかね…。

仲澤@失業者 2008/12/01(月) 16:58:26
> これと同じような挙動にしておきたいなと思い、

これは多分「削除予約」型の動作かと思います。
DeleteItem( h)で、「h」の意味するオブジェクトを
リアルに削除せず、「h」のメンバの「削除予約フラグ」を
立てるだけで済まします。このオブジェクトはメモリー上には
存在しますが、論理的に削除済みなわけです。
そうでないと、「h」に対応するオブジェクトがリアルに
存在しないことを証明するため、配列内の全オブジェクトを
トレースしなければならず、不経済です。
こう言っちゃなんですが、これではちょと頭が悪いかも(笑)。

当然、ガベージコレクションは別の機会に行わなくてはなりません。

亀山 2008/12/02(火) 09:33:42
> これは多分「削除予約」型の動作かと思います。

これもかなりひねくれた例ですが、

m_tree.SetItemText((HTREEITEM)0x12345678,  _T("SetItemText"));

というように、適当なハンドル値を入れてもちゃんとFALSEが返ってきます。
これも含めて真似るために、「頭が悪いかも」のやりかたをしていたのですが、
削除予約型のときは、このようなメモリ上に存在しないもののときに
正しくエラーを返すことができなくありませんか?

もちろん、ツリービューがHTREEITEMをどのように扱っているのかは
aetosさんの言われるように不明なので、実際には内部を行き来せずに
判定できる方法を備えているのかもしれませんが、常識的にはこのような場合、
削除されたアイテムや存在しないアイテムを指定されたときは
わざわざ丁寧にFALSEを返してやらなくてもいいものでしょうか?

aetos 2008/12/02(火) 09:45:41
ハッシュテーブルのように、全数探索しなくても高速に、対象のアイテムがあるかどうか
判断できる方法はありますけどね。

> 常識的にはこのような場合、
> 削除されたアイテムや存在しないアイテムを指定されたときは
> わざわざ丁寧にFALSEを返してやらなくてもいいものでしょうか?

常識ねぇ…。
個人的には、変な値を渡した時に例外出して落ちちゃっても、常識はずれだとは思いませ
んけどね。

PATIO 2008/12/02(火) 09:55:35
HTREEITEMが管理されたハンドルで有れば、実際に使われている物かの判断は
出来るはずなので、こういうことがしたいのであればちゃんと管理するしか無いと
私は思います。実際に実在チェックを現実的なスピードで行なえるように
管理の方法も考える必要があるでしょう。
単なるポインタだけでこれをやろうとするから無理があるのではないでしょうか。
HTREEITEMの話にしてもきちんと解析した結果、そういう実装になっているという
話では無いのでこれを例にしてもあまり意味がないように感じます。

maru 2008/12/02(火) 09:57:25
> HTREEITEM hItemParent = m_tree.InsertItem(_T("Parent"));
> HTREEITEM hItemChild  = m_tree.InsertItem(_T("Child"), hItemParent);
> m_tree.DeleteItem(hItemParent);
> m_tree.SetItemText(hItemParent, _T("SetItemText"));
> m_tree.SetItemText(hItemChild,  _T("SetItemText"));
> 
> のSetItemText()で親も子もちゃんとFALSEが返ってきます。
> ハンドルにはInsertItem()の戻り値が入ったままです(すでにゴミ)。
これはm_treeのメンバー変数に有効なアイテムハンドルのリストが格納されていて、
渡されたアイテムハンドルがそのリストに存在しているかだけを判定しているのでは
ないでしょうか?
だいたい、aetosさんも言っているけどHTREEITEM はハンドルであってポインタでは
無いのだから。この辺りを混同しているのではないかしら。

> 常識的にはこのような場合、
> 削除されたアイテムや存在しないアイテムを指定されたときは
> わざわざ丁寧にFALSEを返してやらなくてもいいものでしょうか?
場合によるでしょう。
不特定の人が使用するライブラリの場合、身を守るために引数のチェックを厳密に
行なうこともあるだろうし、逆に効率を優先する場合だってあるだろうし。

tetrapod 2008/12/02(火) 10:05:32
> わざわざ丁寧にFALSEを返してやらなくてもいいものでしょうか?
この辺は C/C++ 言語規格書でいうところの「未定義の動作」になる話で、
未定義=誤ったプログラム断片に対する動作であって、何が起こるか不明なもの
・暴走してもいい (Release 版はたいていこういう動作をする)
・エラーを検出してプログラムが強制終了させられてもいい
・正しくエラーを検出してもいい(たいてい既に手遅れだったりするが)
・鼻から悪魔が出てもいい
というやつだ。
デバッグ版ではデバッグの助けになるよう3番目の動作をしてくれれば理想だが
そのために処理が遅くなって、メモリ消費量も増大して、所定の性能が出ない。
リリース版は、十分にデバッグがなされている前提でこういうチェックを行わない。
そのぶん速く、メモリ消費量も少なく、想定される性能が出る、ということ。

俺なら、リリースまでには十分にデバッグされている前提で
リリース版ではわざわざ FALSE を返してやるほど親切なことはしない。
デバッグ版ではどうするかな?
チェックを入れてやるかもしれないし入れないかもしれない。
一緒に開発する周りのレベル次第で判断するだろう。

もし、エラーチェックを入れるとしたら、の話をするなら:

CTreeCtrl の中身は MFC ソース見ればわかるとおり、実のところ
SendMessage の wrapper にすぎないわけで、実処理がどうなっているかは
公開されていないわけだけど

とりあえず Win32 限定でよければやはり MFC ソースにいろいろヒントがあり
AfxIsValidAddress とか AfxIsMemoryBlock とか、その辺で
 *不完全ながらも* 調べることができる。
# デバッグ版のみだけどな
# リリース版に入れる必要はないわけで、当然の措置だが
いろいろ入れておけば、少しはデバッグの助けになるかもしれない。
リリース時には除去しておくことを推奨する。

tetrapod 2008/12/02(火) 11:54:15
っていうか、デバッグ目的であるなら

渡されたデータにプログラム論理上の明確な誤りがあるとわかった時点で
バグなんだからその場でエラーログを出力して即死すべき、だろう。

渡されたデータに操作員のミスによる誤りがあるなら、エラーで継続すべきだけど。
これはバグぢゃないわけで。

delete 済みポインタとか明らかにオブジェクトをささないポインタとかを渡す
ってのは前者であって ASSERT 類で即死すべきバグ。
その場はなんとなく継続して動くほうがよっぽど有害だと思う。

その意味で
> わざわざ丁寧にFALSEを返してやらなくてもいいものでしょうか?
バグに対しては即エラー検出、終了のほうが有意義。

亀山 2008/12/04(木) 11:31:29
[[解決]]
みなさん解説ありがとうございます。

ポインタ自体の値からは存在自体を判断できないとわかりましたので、
「最近使われたファイル」のように
追加やアクセスをされたポインタを特定数だけキャッシュしてみたり、
全ポインタを1次元配列で別に持たせてみたりという処理を試してみます。

それでもあまり効果が見られない場合には、
存在チェック関数自体を外に出して、チェック自体は利用側の判断に任せ、
実際の操作関数内ではノーチェックでやってしまおうかと思います。

古いスレッドには追加発言できません。