[C++] NULLポインタアクセスをMFCはどのように回避して…いないか。

null/nil の扱いをオブジェクト指向的に考え直す | Moonmile Solutions Blog
http://www.moonmile.net/blog/archives/3770

なところで、Objecitve-C の nil の「優れた」扱いについて書きましたが、実は MFC(Microsoft Foundation Class)にも NULL ポインタに対して「優れた」扱いをしているのです、という話をひとつ。

MFC ってのは、Windows アプリケーションを作る時のクラスライブラリで、似たようなものには Delphi のライブラリがあります。ウィンドウシステムを扱うときに、直接の Windows API を扱うよりもクラスを使ったほうがまとまりが良い、ってのが主旨ですね。これを押し進めたのが、Windows RT なんですが、またこれは別の機会に(というか、この話はあちこちで出ているので)。

オブジェクト指向の扱いとして、MFC が行った実装は、CWnd クラスになります。

CWnd クラス (MFC)
http://msdn.microsoft.com/ja-jp/library/1xb05f0h(v=vs.80).aspx

CWnd クラスってのは、あらゆるウィンドウの元のクラスになっていて、これは Windows OS が、ウィンドウハンドル(HWND)を必ず持っている、という前提から来ているものです。後に、WTL というテンプレートライブラリで作ったウィンドウクラスライブラリもある訳で、必ずしも HWND が必要という訳ではないのですが。
CWnd クラスは、内部的にひとつのウィンドウハンドルを持ちます。CWnd::m_hWnd という形で保持しているのですが、このメンバ変数へのアクセスは、CWnd::Attach, CWnd::Detach という特別なメソッドを使います。あるいは、ウィンドウを作るための Create メソッドを使います。

ここで、CWnd 自体と、内部の CWnd::m_hWnd の動きを見てみると。

CWnd wnd ; 				// m_hWnd: NULL;
wnd.Attach( hwnd );		// m_hWnd: hwnd ;
...
wnd.Detach();			// m_hWnd: NULL;

という感じになります。クラスとして m_hWnd を内部に持っているために、外部としては常に CWnd オブジェクトを持ち続け、Attach/Detach により 内部の m_hWnd を設定する≒遷移するという仕組みです。

さて、ここに CWnd::GetClientRect というウィンドウの矩形の大きさを取るメソッドがあります。GetClientRect メソッドは、内部的に m_hWnd を参照して、Windows API の GetClientRect 関数を呼び出すという仕組みになっているので、コードとしてい次のように書きます。

CWnd wnd ;
wnd.Attach( hwnd );
CRect rc;
wnd.GetClientRect( &rc );
...
wnd.Detach();

GetClientRect 関数は、必ずウィンドウハンドルを必要とするのですが、CWnd オブジェクト自体が NULL でなければ、次のように Attach 前の場合にも呼び出しは可能です。

CWnd wnd ;
CRect rc;
wnd.GetClientRect( &rc );
...

さて、このとき rc は何を期待するのでしょうか?
内部のウィンドウハンドルが初期化されたまま NULL のままなので、矩形の大きさの取得自体が無効です。無効ではありますが、あえて返すとすれば、{0,0,0,0} の無効値を返すのがよいでしょう。

ここで面白いのは、ラップしている CWnd クラスと、ウィンドウハンドル自体の m_hWnd の関係です。ウィンドウハンドルに関わるメソッドについては、CWnd オブジェクトからの呼び出しは可能であるのですが、内部の m_hWnd が NULL の場合は「アプリケーション例外」を発生させるべきかどうか?という問題がでてきます。

そう、Objective-C 風にするならば、m_hWnd の値が NULL の時(Attach する前の時)でも、CWnd::GetClientRect はさらりと呼び出されたほうがよいはずです。そうですよね…

■で、実際のところはどうなのか?

実は、CWnd クラスの内部で、ASSERT( this->m_hWnd != NULL ) という形でチェックをしています。わざわざデバッグモードでは止めてあるのです。しかも、ASSERT はデバッグコンパイルだけ実行されるので、リリースビルドの場合は m_hWnd は NULL のまま通ってしまい、アプリケーションエラーとなります orz

ここのところは if ( this->m_hWnd == NULL ) return ; のように、NULL チェックだけして何事もなかったように値を返して欲しかったなぁと。そうすると、Attach や Create 前に wnd->GetClientRect をしてもエラーにならなくて便利なんですけどね。

カテゴリー: C++ パーマリンク