プログラミング所感 - 前へ 目次 次へ


00/2/9(水)

バグではないが VCにまつわる話を、といいつつ。

いつのまにやら、2000年も過ぎた。2ヶ月以上ぶりの更新である。 家でも、1/1 を前に、食料を多めに買ったり、水をペットボトルに溜めたりした。 が、殆ど何も起きなかったね。只、原子力発電所関係で 2000年問題が発生したのには、はっきりいってあきれたね。 いくら関係ない所とはいえ、あれだけ、はっきり 「絶対に大丈夫です」と言い切った手前どうすんだ? (だいたい、いくら税金使ったんだ?) まあ、何も責任とらないと思うけど。でも責任とって欲しいね。 大丈夫だって言ったんだから。認識甘いというか、往生際が悪いというか。

話がのっけからずれてしまったけど。 今回は、VC で DLL を作っていたときのこと。始めは問題なかったのだが、 突然、「DLLMainが、Mfcs42.lib と MSVCRT.lib の両方で定義されている」と 出てリンクできなくなった。さっぱり分からなかったのだが、 ふと、stdafx.h を使ってないソースにも全部 stdafx.h を書いたら 直った。理由はなんとなく想像できるが、はっきり説明できない。 動きが挙動不審なんだよなあ。 直ったからまあいいか。

ちょっと錯乱気味に、また、話は変わるけど。ニフティのログロックの コンテストを しているサイトがあって、現在、14問出題されている。
で、殆ど私がダントツで Top とってしまったのだが、実は、 アルゴリズムで解かせているからだ。でも、私以外についてきている 人がいないので、他の人は人間が解いているようだ。 プログラム書いて解こうという人はいないのかねえ。 もうちょっといてもいいと思うんだが。大学生とか。
(今回はぐちっぽいな。)

P.S.後から気づいたのだが、件のページには「アルゴリズムで解くな」と警告があった。 始めはなかったと思うのだけど。私のせいでできた警告かもしれない。 まあ、あれだけ瞬時に最短手を解答していたら気づくでしょうね。


00/2/16(水)

真円と楕円について。 かつて、真円(Circle)と楕円(Ellipse)を巡って、 データ設計上の意見の対立があった。
1つは真円から楕円を派生させる方法(真楕式)であり、 もう1つは楕円から真円を派生させる方法(楕真式)である。

真楕式の言い分は「真円のメンバ(中心、半径)に対し、 楕円のメンバ(中心、長径、短径)は増えている」というものである。 しかし、これは間違いである。 第一に、半径は長径でも短径でもない。 つまりメンバは増えたのではなく、増減しているのである。
第二に、真楕式は派生の原則に反している。
派生の原則とは「A が B から派生しているならば、 A is a B の関係が成立しなければいけない」というものである。 数学的事実として、真円は楕円である。(何故なら、 楕円の定義を真円は満たしている。) 従って、原則に沿うためには楕円から真円を派生すべきである。 そうすれば、オブジェクト指向のメリットを受けられる。
例えば、次のような面積を求める関数があったとしよう。
double Ellipse::Area()const {
    return PI * Radius1() * Radius2();
}

この関数は、真円でもそのまま使用できるため、派生クラスで 再定義する必要がない。それでは、楕真式のインターフェースを見てみよう。
class Ellipse {
    double  rad1;
    double  rad2;
    Point   pt;
public:
    void SetRadius1(double r){rad1 = r;}
    void SetRadius2(double r){rad2 = r;}
    virtual double Radius1()const{return rad1;}
    virtual double Radius2()const{return rad2;}
    double Area()const;
};

class Circle : public Ellipse {
public:
    void SetRadius2(double r){rad1 = r;}
    virtual double Radius2()const{return rad1;}
};

さてさて、真楕式の賛成派は、上で定義した Circle のオブジェクト サイズが無駄に大きくなると、異を唱えるだろう。それを避けるためには、 次のようにすればよい。
class AbsEllipse {
public:
    virtual void SetRadius1(double r);
    virtual void SetRadius2(double r);
    virtual double Radius1()const;
    virtual double Radius2()const;
    double Area()const;	// ここで定義
};
class Ellipse : public AbsEllipse {
    // ....
};
class Circle : public AbsEllipse {
    // ....
};

しかし、この設計でも次の批判は残ってしまう。 しなわち「Circle が Fat Interface である」というものである。 確かに、Circle のインターフェースには無駄がある。 この無駄を取り去るには派生関係をなくすよりしょうがない。 しかし、楕円が利用できるものは(理論的に)真円でも利用できるので、 全くの無関係にすべきものでもない。

結局、次のようにするのがベストであろう。
class Circle {};
class Ellipse {
public:
    Ellipse();
    Ellipse(const Circle&);
};

こうすれば、Circle のオブジェクトでもキャストすることにより Ellipse のメソッドが使用できる。また、この方法は、仮想関数を 使用しないので効率が良いし、メモリの節約になるかもしれない。
ちなみに、Ellipse(const Circle&) に explicit は付けない方がよいと思われる。 また、Ellipse のコンストラクタを用意する代わりに、 Circle に operator 演算子を用意してもよい。 どちらを選ぶかは好みの問題であろう。

この方法は、生成式と呼ぶことにしよう。
生成式の特徴としては、

  • A is a B の関係が成立する。
  • A は B に条件(制約)がついたものである。
  • A のメンバより B のメンバの方が多い。
というのがある。 また、A is a B であるから、B で用意されているものは A でも使用できるが、 A の方がシンプルなため A 独自のものを用意した方が効率がよいことがある。
例えば、実数と複素数の関係もこれにあたる。complex<T> sin(const complex<T>&) があれば、double sin(double) の代用になるが、double sin(double) を用意した方が便利なのは 明らかである。

00/2/28(月)

ダイアログバーのボタンについて。 ヘルプには「ダイアログバーはそのまま使え」とあるが、以下の話では派生して使っている。 「ダイアログバーにボタンをつける」ということは普通はしないと思うので、あまり役立たない情報かもしれない。
今回は、必要があって作ったのだが 3つ問題が生じた。

第1の問題は、ビットマップボタンにしたかったのだが、OnInitDialog がフレームワークから呼ばれない。 調べるとダイアログバーの場合、メッセージマップで

	ON_MESSAGE(WM_INITDIALOG, HandleInitDialog)
として、
	LRESULT HandleInitDialog(WPARAM,LPARAM);
を代わりに使うようだ。 第2の問題は、せっかくHandleInitDialogを作ったのに、ClassWizard でボタンに割当てた変数に対し SetBitmap が失敗してしまう。どうも DDX がちゃんと動いていないようだ。 まあ、派生して使うなということだから仕方がない。これは、直接
	((CButton*)GetDlgItem(IDC_BUTTON1))->SetBitmap(bmp);
として回避できた。

第3の問題は、最も深刻だった。ボタンが押せないのである。 他の人が作っているところで、できているようなので調べたら次のようにしてできた。 しかし、本人も何故そうしたのか覚えていないとのことだ。解決方法は、 「そのダイアログバーを持っているフレームクラスで、ボタンのハンドラを定義する」である。 このハンドラはダミーなので中は空でよい。
つまり、ダイアログバーの処理は、親フレームで全て処理することが期待されている。 それで、ボタンを押せるかどうかは、親フレームにハンドラを定義しているかどうかで 判断しているようだ。そこで、ダミーで定義すれば押せるようになると思われる。 でも実際に呼ばれるハンドラは、ダイアログバーで定義したハンドラである。 ある意味、フレームワークを騙しているのだから、危険なトリックかもしれない。


00/2/29(火)

ビットマップの背景透明化について。 gif だと、背景色という設定があって、WebPage でよく使ったりするが、 MFC で ビットマップの背景透明化の方法を後輩が開発したので紹介する。

サンプルの作成

  • 新規に AppWizard(exe) で BitmapTest を作成する。SDI でそのまま OK。
  • 新規にビットマップリソースを作成。 コピーして、絵を黒で塗りつぶし、マスク(IDB_BITMAP2)とする。 元絵(IDB_BITMAP1)は、背景を黒くする。
  • ClassWizard で CBitmapTestView の WM_LBUTTONDOWN にハンドラを作成。
  • CBitmapTestView.cpp を以下のように修正
    CPoint	g_pt;
    void CBitmapTestView::OnDraw(CDC* pDC)
    {
    	CBitmapTestDoc* pDoc = GetDocument();
    	CBitmap	b1,b2,*p1,*p2;
    	BITMAP	bm;
    	b1.LoadBitmap(IDB_BITMAP1);
    	b2.LoadBitmap(IDB_BITMAP2);
    	b1.GetBitmap(&bm);
    	CDC		dc1,dc2;
    	dc1.CreateCompatibleDC(pDC);
    	dc2.CreateCompatibleDC(pDC);
    	p1 = dc1.SelectObject(&b1);
    	p2 = dc2.SelectObject(&b2);
    	CBrush	br(HS_CROSS,RGB(200,255,200));
    	CRect	r;
    	GetClientRect(&r);
    	pDC->FillRect(&r,&br);
    	pDC->BitBlt(g_pt.x,g_pt.y,bm.bmWidth,bm.bmHeight,&dc2,0,0,SRCAND);
    	pDC->BitBlt(g_pt.x,g_pt.y,bm.bmWidth,bm.bmHeight,&dc1,0,0,SRCPAINT);
    	dc1.SelectObject(p1);
    	dc2.SelectObject(p2);
    }
    
    void CBitmapTestView::OnLButtonDown(UINT nFlags, CPoint point) 
    {
    	g_pt = point;
    	Invalidate();
    }
    
  • ビルドして確認。CBitmap の変数は、適当なところにメンバ変数にした方がよいだろう。 ミソは、マスクで書きたいところを消しておいて、書きたいものを OR で書くところであろう。 StretchBlt でもよい。

00/3/10(金)

3/8朝、営団で始めての死傷事故が起きた。
脱線した車両が対向車両と衝突したのだ。 東京新聞記事

二日後の今日、営団は護輪軌条の基準見直しを検討している。 (読売の記事では 補助レールと記述しているが、護輪軌条という方がかっこいい。)
護輪軌条とは、電車が脱線しないようにレールの内側につけるもう一つの レールのことである。車輪をサンドイッチにして外れないようにするものだ。 これまで、見たことはあったが、名前も役割も知らなかった。 しかし、このレールがあれば大惨事は防げたかもしれないのだから、 役割は重要である。

この雑記はプログラミング所感であるから、プログラミングの世界での 護輪軌条は何かを考えてみよう。
まず、思いつくのは assert であろうか。しかし、assert は事態がまずくなってから、 いわば、事故が起きたときに検出するものなので、「事故を未然に防ぐ」護輪軌条とは 異なるといっていいだろう。
これというものは、なかなか思いつかないが、適当にあげていってみよう。
データ構造の設計では、護輪軌条のようなものが存在している。 1つは、データの冗長性を排除した設計があるだろう。データの整合性が崩れる (事故が起きる)ことをそもそも不可能にするという点で、護輪軌条と同じといえる。 よく行うものでは、エラーチェックもそうかもしれない。例えれば、電車の前方に 障害物を発見して、ブレーキを踏むようなものであろうか。 (assert は、abort() を呼ぶから自爆するようなものだ。もちろん、 初心者のための提言で書いたように、 どんどん使った方がよいけど。)

他には、ライブラリやフレームワークやデザインパターン、 あるいは整った開発環境の利用もあげられるかもしれない。
適切なレビューなどはどうだろう。まぁプログラミングに入ってから レビューっていうのは変かな。

ソフトウェア設計では、バグの検出に重点を置いている気もするが、 バグそのものを出さない護輪軌条はもっとないのだろうか。
もっとも、私はいつも次のように思っているのだが。

「バグは無限に存在する」

8/18 の話参照。

00/3/13(月)

ファイルフォーマットのバージョン管理について。
GUI アプリは、だいたい、VC++ で AppWizard で作るので、 ファイル管理は、Serialize 関数を書くぐらいで済んでしまう。
これまではそんなにヘビーに使われるものを作らなかったので、 ファイルのバージョン管理などしていなかった。 あるプロジェクトでは、必須の機能になったので入れたのだが、 要は、ar << 100; とか最初にバージョン番号を入れるだけである。 めちゃくちゃ単純だが、これがあれば後でどうとでも対応できる。 これがないと、後でバージョンアップするときにコンバータなりが 必要になってくる。 だから、これからはいつもバージョン番号を 最初に書くようにした方がいいかもしれない。 手間もサイズも殆ど変わらないし。 ちなみに、CObject から派生していれば、バージョン管理ができる(はず)。 しかし、ここでいっているのは、ファイルフォーマットのバージョンの話である。 CObject のバージョン管理は個々のオブジェクトの話であって、このオブジェクトが 増減したり、順番が変わったりしても対応できるはずがない。と思っているのだが あっているかな。CDocument は、CObject から派生しているからできるのかな?
5/22 の話参照。

00/3/14(火)

最近、Java を勉強するようになった。
動機はやはり Applet を作りたいからである。まずは、Webサイトで 勉強しようと思ったのだが、どうもよくわからないことが多くある。 それで、本をいろいろ探して買ったのだが、読んでみると JDK 1.0 しか 出てこない。さすがにしょうがないので、本を取替えてもらった。 今度は最新のものをと思って、Java2 の本にした。発行日は先月だから、 かなり新しい。名前は「コアJava2 Vol1」である。値段は、6800円と高いが、 非常によい。付録もよい。TextPadが便利である。80MB 以上もあるけど リファレンスがついているのもうれしい。
まだ、最初の方しか読んでないが、これまでの疑問が氷解していく。 確かに、Java は C++ に比べればやさしいのがわかってきた。

さて、ここからが本題。

「C++ は Java に取って代わられるのであろうか」
私はそうは思わない。確かに、Java のメリットは数多くある。 しかし、C++ のメリットも厳として存在する。C++ とJava では、 目指しているものが違うと感じる。だが、より合うことはできるであろう。 Java は、C++ を土台にして作られた。次は、C++ が Java に影響されて 変わるべきだろう。(というより、変わってくれると、C++ と Java の 両方を使っても混乱が少ないだろう、という自分勝手な考えなのだが)
  • Java のように、捨てるべき言語機能は捨てるべきだ。
  • Java のように洗練された所は、取入れるべきだ。
  • STL は、まだ改良の余地がある。
  • GC は必要ない。何故なら、私は今現在、GC なしでうまくやっていけている。
問題は、旧C++ との互換性だろう。C++ の名前を残すならば、互換性はある程度残すべきだ。 また、C++ と Java を mix した別の言語として、名前を変えるということも 考えられる。私としては、その方がいいと思う。但し、これまでの、膨大な C++ の ライブラリやソースを利用できる手段は必要であろう。

また「よい言語が大衆に受入れられるとは限らない」ということもある。 OS もそうだけど。

ちなみに、前に書いた、内部クラスに関する疑問は、 上記の本を読んで理解した。結局、「内部クラスは、外側のクラスの参照を隠れて 持っていて使える」ということだった。
7/14 の話参照。


00/3/27(月)

書くのも恥ずかしいのだが、大失敗をした。
UNIX の 「rm -r」と同じことをする関数が必要だったので、 Windows API を調べたが、なさそうだったので自作した。
適当に作って実行したのだが、終わらないのでデバッガで調べてみると、 なんと!ディレクトリを上へ辿っているではないか。E ドライブで 実行していたのであるが、E ドライブ直下のファイルが、1つを残して 全部消えてしまっていた。顔面蒼白である。結局、何が消えたのかは 思い出せないのでわからない。あやふやな記憶では、重要なのはなかったと 思う(思いたい)のだが。。。

00/3/28(火)

STL のコンテナの選択について一言。
例えば、次のような場合
list<...>	l;
while (...) l.push_back(...);

最初にサイズがわからない場合、list を使いたくなることがあるかもしれない。 しかし、途中に挿入や削除をしないのであれば、vector を使うべきである。 vector の方がメモリの点でも速度の点でも効率的なのは説明するまでもないであろう。 さらに、vector の反復子は ランダムアクセス反復子であるから恩恵もうけられる。 (例えば、stable_sort ができる。list を stable_sort したければ、一旦 index を vector で作り直すなどしないといけない。)

vector で push_back を使う場合でも、最初にサイズがわかっているならば、 reserve を使うべきなのは言うまでもない。

次に、何でもかんでも map を使いたがる人が多いように感じる。 例えば、名前と社員コードがある「社員(Employee)」を作ったとすると
class Employee {
    string name; // 名前
    ...
};
    map<int,Employee>  data;

全体のデータを map で管理したりしていることがある。 これは、次のように直すべきだ。
class Employee {
    int    code; // 社員コード
    string name; // 名前
    ...
};
    map<int,int>      dic;
    vector<Employee>  data;

dic は社員コードから index を変換するために用いる。 後は、index を使って data をアクセスするようにするのだ。 もちろん、アクセスのたびに dic を使っていたら元も子もないので、 極力 index のみ使うようにしなければいけない。

map は便利だが、まず vector が使えるか考えること。 但し、set の代わりにいつでも vector を使うのはやめた方がいいだろう。 おそらくは、よく考えれば set か vector のどちらを選ぶべきかは わかるはずだと思う。


00/3/31(金)

インターフェースについて。
Java で標準出力に出力するとき、
  System.out.println(...)

と書く。実は、こういう書き方が、私は好きである。 最初に、こういう書き方に出会ったのは、14年程前、CLU という言語である。 はっきり言ってマイナーな言語であり、私も殆ど覚えてないのだが、 Java と同じような書き方になっていた。もっとも、Java の out はオブジェクトだが、 CLU では(C++風に言えば) static な関数だったと思う。

基本的には、私はタイピングが少ない方が好きである。 しかし、C ならば、printf で済むところを、2,3 倍のタイピングを要するものを 何故、好むのか?
それは、わかりやすいからである。物事を分類しているからだ。

printf は、グローバルな空間にボンと置いてある。System.out.println で、 グローバルなのは System だけである。
タイピング量は、あまり問題ではない。というより「わかりやすさ」の方が 問題なのだ。実際、CLU 使用時は、自作エディタの自動展開機能を利用していたので 3,4 文字打つだけで入力できていた。(vi でもできるし、VC++6 なんかでも 途中まで打てばいいようになっているのもある。)
通常だと、文字数がすくない方がわかりやすいといえる。しかし、 この場合は、逆に文字数が多い方がわかりやすくなっているのだ。 最も、out の立場を尊重して、printf との比較ではなく、 fprintf(stdout,...) との比較ならば、文字数はそれほど変わらなくなる。 (さらに言えば、printf と fprintf(stdout,...) の比較では単純に文字数の多い fprintf の方がわかりにくい。)

これらは、つまるところ、名前空間(namespace そのものではなくもっと一般的な意味)の 導入にある。私の好きなのは、それが、標準ライブラリでも一貫して使用されている というところである。


00/4/5(水)

わかりにくいコーディングについて。
以前、「&& を if の代わり」に使って短く コーディングできるという話を書いた。 その後、fj.comp.lang.c で似たような話題が出て、ほぼ全ての人が 「わかりにくいのでやめた方がいい」というものであった。賛成派は 私一人ぐらいだったが、本当のところ、私もあまり好ましいとは思っていない。 基本的なスタンスは、「シンプル&イージー」といったところか。では、 「コンプレックス&イージー」と「シンプル&デフィカルト」ではどちらを選ぶか? これは、ケースバイケースであろう。fj.comp.lang.c で、言いたかったのは、この 「ケースバイケース」である。そこで、常に、否定的であるような意見に対し反論した のだが、意図を正しく伝えられなかったかもしれない。まあ、私の意見もまとまっては なかったのだが。

同じような話に、「カンマ演算子の使用」がある。カンマ演算子を使用すると プログラムを短く書くことが可能である。しかし、たいていの場合、カンマ演算子を 使うとわかりにくい。 (C++ ではカンマ演算子をオーバライドできるのでさらにわけわからなくすることもできる。)
結論から言えば、カンマ演算子は原則として使わない方がいいだろう。私は、1つの例外を除いて めったに使わない。例外は、
  for (k=0, it=a,begin();it!=a.end();++it, ++k)

のような for の中である。ここでは、2つカンマ演算子を使っている。 1つめの k=0 を書いている理由は ++k を同じところで書いているので、 対比させるためである。2つめの ++k を書いている理由は、別のところに書くと わかりにくくなることがあるからである。また、この場合は、スペースの入れ方にも ちゃんと意味がある。カンマ演算子のところにスペースを入れることにより、 通常とは異なる記述方法を目立たせているのだ。
カンマ演算子は、バグの原因にもなりやすい。 例えば、ある人のバグで次のようなのがあった。
  if (func(a,b),0) {

書いた本人は、func(a,b,0) のつもりだったのだが、 第 3 引数がなくてもよかった(デフォルト引数 or オーバロード)ため、 コンパイルでは何のエラーにもならなかったのだ。

ところで、次のような表記は欲しくなったりもする。関数が複数の返り値を持てて、代入もできるというもの。
  a,b = func();

但し、上の例は、コンパイルできる可能性があるが、もちろん意図した通りにはならない。 これ以上、C++ を複雑にしてどうするというのもあるけど。


00/4/7(金)

反復子のシリアライズについての話は、 あまり聞かないが自分でちょっと考えてみた。 (本当にちょっと考えただけなので、たいした話ではない。)
まず、次のクラスのオブジェクトをシリアライズしたいとする。
   struct Some {
     list<int>           l;
     list<int>::iterator i;
     void Save(ostream& o);
     void Load(istream& i);
   };

i は、l の中のどこかを指しているとする。(すなわち、Java のシリアライズみたいに 反復子の指している先を自動的に生成することは考えない。) どういう実現方法がいいのかよくわからないが、取りあえず、こんなのを書いてみた。
   #define forall(C,I) for (I=C.begin();I!=C.end();++I)

   namespace iterserial {
   template <typename T,typename C,typename I>
   void write(T& t,C& c,I i) {
     I   it;
     int v = 0;
     forall(c,it) if (it == i) break; else ++v;
     t << v << " ";
   }
   template <typename T,typename C,typename I>
   void read(T& t,C& c,I& i) {
     int v;
     t >> v;
     i = c.begin();
     while (--v >= 0) ++i;
   }
   }

   void Some::Save(ostream& os) {
     list<int>::iterator  it;
     os << l.size();
     forall(l,it) os << *it << " ";
     iterserial::write(os,l,i);
   }
   void Some::Load(istream& is) {
     list<int>::iterator  it;
     int v;
     is >> v;
     l.resize(v);
     forall(l,it) is >> *it;
     iterserial::read(is,l,i);
   }

こんなのでも役に立つかな。
CArchive を使うときはデリミタをとらないといけない。また、ランダムアクセス反復子用に テンプレートの特別バージョンを書いた方がいいだろう。(I ではなくて I* を用意する。) あるいは、STL の distance を使えばいいかもしれない。distance には 特別バージョンが存在しているので。


00/5/9(火)

昔のプログラムのバグ取りをしてたら、また間違えていた。 四捨五入を i=(int)(x+.5); としていた。 (正しくは、i=(int)floor(x+.5); 負の場合にずれる。)
「三つ子の魂百まで」か。基本的な所は最初にちゃんと覚えないとダメだね。

00/5/10(水)

デフォルト引数の話。
我ながらまずい設計だと思ったのが、これ。
struct Any {
    void foo(int i,int j);
    void foo(int i=9){foo(i,i);}
};

最初、2引数の関数だけを用意したのだが、便利かと思って1引数のも追加した。 さらに、0引数も追加しようかと思ったのだが、1引数のデフォルト引数にした。 こうすると、

  • foo(1); -> foo(1,1);
  • foo(); -> foo(9,9);
というふうに、両方とも第2引数を省略しているのに、 違う値になってしまう。これは、わかりにくいだろう。 ということで、次のように直した。
struct Any {
    void foo(int i=9,int j=9);
};

ちなみに、私はあまりデフォルト引数を使うのは好きでない。 プログラムが固定してしまったら、使うのはやぶさかでないが、 引数を変更する可能性があるときは、デフォルト引数があると 混乱しやすい。
引数を増やすときに、「既に書いてしまった 関数呼出しを修正しなくてよいから」という理由で デフォルト引数を追加する人もいるが、私はわざと避けるようにしている。 理由は、やはり思わぬバグの原因になりやすいからだ。


00/5/11(木)

ちょっと思いついて計測してみた。
今回で62回目。書き始めてちょうど2年半だ。年度別では、
97年98年99年00年
2回29回17回15回
曜日だと
1回21回9回13回6回12回1回
さていつまで続くか。

話は変わるが、記録しとけば良かったと思うのは、である。 理由は、本屋で買いたいと思った本を既に読んだことがあるかどうか 分からないからだ。子供には「自分が読んだ本を記録する」習慣を教えるようにしよう。 私には、既に買ってある本のデータベースを作る気力はない。 というか挫折した。たぶん、単行本だけで2,3千冊あるだろう。

また話は変わるが、初心者に GUI アプリケーションの講習をする時に、 題材として何がいいだろう。VC++ チュートリアルでは、 お絵かきソフトを作らせている。あれは、いい題材だと思う。 簡単に高機能なものができたので感動した覚えがある。
他には、エディタなどどうだろう。VC++ で AppWizard なら楽々できるだろう。 WebBrowserコントロールで単語をWebで検索なんかもできるだろう。 後は、Exploler みたいなファイラーもいいかもしれない。 機能的には、「ツリービューで複数フォルダの選択すると、 全部フラットに見える」とか「サブフォルダ以下もフラットに見える」 というのが欲しいな。


00/5/15(月)

再び、参照メンバの話。 参照メンバを使用していたらどうも思った通りに動かない。 もちろん、以前の話の原則
  • 参照メンバは自クラスのメンバの別名となるべき
に則っている。原因は、単純にコピーコンストラクタを 定義していなかったからだった。
また、
  • 参照メンバがあると、デフォルト代入演算子ができない
は、VC++ 固有の仕様かもしれない。 現在、もっとも信頼できる「プログラミング言語C++第3版」にも 記述がなかったように思う。この「固有の仕様かもしれない」という 状態を避けるため、参照メンバを使用したら、デフォルトに頼らず、 必ず代入演算子を宣言するようにした方がよいと思う。 (もし、必要なかったら宣言のみで未定義のままにすればよい。)

では、参照メンバに関する鉄則を決めよう。

  • 参照メンバは自クラスのメンバの別名にする
  • コンストラクタ、コピーコンストラクタは、適切に定義すること
  • 代入演算子を宣言すること
さて、何故、参照メンバを使用したかというと、 各々のメンバを添え字でアクセスしたかったからだ。
struct Test {
  enum {Size = 3};
  int data[Size];
  int& height;
  int& width;
  int& id;
};

という具合。data[0],data[1],data[2] が height,width,id に相当する。
以前は、「参照メンバなど殆ど不要」と思っていたが、 なかなか使い所があるかもしれない。


00/5/22(月)

再び、ファイルのバージョン管理について。 にも書いた話の続き。 フォーマットの最初にバージョン番号を入れておくと、 「アプリケーションがバージョンアップしても旧ファイルを読める」 ようにできる。そこで、もう一歩勧めて、「旧アプリケーションが新ファイルを 読んだら、その旨の警告を出す」ようにしておいた方がいいだろう。 できれば、更新方法の情報が出ればもっといいだろう。ネットにつながっていれば、 自動更新もいいかもしれない。ウィルスバスターではできているけど。

00/5/23(火)

VC++ のバグかと思ったのだけど、自分の勘違いだった。 メンバが3つあるやつで、それぞれを第1キー、第2キー、第3キーにして比較 するところを次のように書いていた。
  bool operator<(const Any& a)const{
    return i1 < a.i1 || (i1 == a.i1 &&
           i2 < a.i2 || (i2 == a.i2 &&
           i3 < a.i3));}

この関数は間違っているが、どこがおかしいか判るだろうか?

答えは、次の通り。
  bool operator<(const Any& a)const{
    return i1 < a.i1 || (i1 == a.i1 &&
          (i2 < a.i2 || (i2 == a.i2 &&
           i3 < a.i3)));}

何ともお粗末でした。


00/5/24(水)

Undo について。初の3日連続更新だ。 (ううむ、現実逃避だ。)
Undo の実装は大きく分けて、(1)差分を覚える(2)全部覚えるの 2種類あるだろう。10数年以上前であれば、ハード的な制約から(1)にせざるをえない こともあっただろうが、現在では気楽に、より実装しやすい(2)の方針を取れるだろう。
通常は、Data を必要なデータとしたとき、
  list<Data>            m_data;
  list<Data>::iterator  m_cur;

として、リストと現在のデータを指す反復子で管理できるだろう。 あとは Undo したい操作の直前で現在のデータを複製(MakeClone)するようにする。 実際に Undo するときは、m_cur をデクリメントするだけでよい。 また、Redo も簡単に実装できるだろう。

さて、MakeClone の実装だが、始め次のようにしていた。(細かい所は省略)
  m_data.push_back(*m_cur++);

もちろん、これでも問題なく作ることはできる。しかし、MakeClone するとデータのメモリアドレスが 変わってしまう。結果として、外部にデータを指す反復子を持っていると、更新しないといけない。 そして、更新忘れからバグが生じやすくなっていた。で、ちょっと考えれば次のようにすればよかった。
  m_data.insert(m_cur,*m_cur);

こうすれば、Undo がないときと同じように書いても問題ない。実に簡単。


00/5/26(金)

参照に関する VC++ のバグ。 (惜しいな、昨日書けば5連続だったのに)
#include <cstdio>
#include <utility>
using namespace std;

template<typename T>
T f(const T& t){return t;}

int main()
{
  pair<int,int>  p(1,2);
  pair<int,int>& r = f(p);  // (1)
  int&           i = f(1);  // (2)
  return 0;
}

(2)はエラーになるのに、(1)はエラーにならない。 gcc ならばもちろん(1),(2) ともエラーになる。

よくある間違いで、オリジナルを設定しているつもりで、コピーされた一時変数を 設定していることがある。つまり、参照(&)の付け忘れだ。 ところが、ちゃんと参照を付けているのに、変更されないことがあった。 で、調べてみると、VC++ のバグであった。一時オブジェクトの const でない参照が 取れてしまうのだ。これは、是非ともすぐに直して欲しいなぁ。


00/6/13(火)

再び参照メンバの話。 「STLによるコンポーネントデザイン」という本を読んでたら、参照メンバを使っている所があった。 もちろん、私の前に書いた鉄則に従っているわけではない。 実際の所、ポインタで書いてもいいもので、単に * つけるのが面倒なので 参照にしているような感じしかしない。まあ、参照だと初期化忘れがないというメリットはあるけど。 参照メンバを使っているのは、関数オブジェクトなのだが代入できないのは、やっぱまずいんじゃないのかな。

00/6/22(木)

set の話。
set<Any> を使いたいとき、必要な要件は、Any がメンバ関数 bool operator<(const Any&)const を持つか、グローバルな関数 bool operator<(const Any&,const Any&) が定義されていることである。 このとき、set のメンバ関数 find も使える。
混乱しやすいのが、ヘッダ <algorithm> の std::find である。これを使うためには、 operator== が必要になる。従って、同じ find でも結果が異なるかもしれない。 基本的には、同じ結果になるように operator== を定義すべきである。 もし、そうなっていないと、
  Any a;
  set<Any> s;
  set<Any>::iterator it;
  it = find(s.begin(),s.end(),a);
  if (it == s.end())
    s.insert(a);

このようなときに、insert が失敗することがある。 (insert の返値はチェックしといた方がいいかもしれない。)

set のメンバ関数 insert には3種類ある。ちょっと勘違いしやすいのが、 iterator insert(iterator it, const value_type& x);(1) である。 これは、pair<iterator, bool> insert(const value_type& x);(2) と ほぼ同じである。違いは、insert される x が it の直後にあるときに速くなることだけである。 従って、通常は (1) と find を対で使うべきであろう。


00/6/23(金)

オブジェクト指向の話。
オブジェクト指向の本質とは、「公開していいものはメンバ関数だけでメンバ変数は公開すべきでない。」 と思っている人が多い。それは、違うと思う。
本質は、「再利用しやすいインターフェースにする」ということである。
このとき、多くのメンバ変数は非公開にした方が再利用しやすいが、 公開したままでも構わないものも存在するのだ。 (例えば、std::pair とか。) また、ある局面では、メンバ変数をそのまま公開しても構わなかったが、 別の局面に変わった時に非公開にした方がよいということも出てくる。 それは、必要が生じた時に変えればいいのだ。再利用化とはそういうステップである。
「メンバ変数は全て非公開」より「Keep It Simple, Stupid」の方が重要である。 「メンバ変数は全て非公開」というお題目を唱える人たちは、 そうすべき理由の前提が間違っていることに気づいていない。

00/6/29(木)

目次が微妙に変わったのに気づいただろうか? 実は、自動作成するようにしたのだ。30分ぐらいで作ったが、2000年問題が発生した。 (ファイル名を見れば気づくでしょう)
結構楽だ。最初から、こうしとけばよかった。

話は変わって、VC++ の新顔のキーワードをいくつか。
mutable は、今まで殆ど使ったことがない。 mutable の第一候補はキャッシュであろうか。 基本的に、mutable 的なものを変更するのであれば、非const にして ユーザに意識させた方がいいのではないかと思っていたからだ。 といいつつも、この前、mutable を使った。対象はテンポラリ変数である。 ユーザは mutable だと意識して使えるようになっている。

次に、explicit だが、これは良く使う。基本的にユーザコンストラクタ (デフォルトコンストラクタ、コピーコンストラクタ以外のコンストラクタ。たった今命名) では付けるように考慮すべきだろう。
あと、typename というのもある。これは、template<typename T> みたいな ものしか使ったことがない。STL 関係ではよく見かけるけど。

全然関係ないが、オープンソースで簡易 Visual SLAM みたいなものを作るかな。


00/7/6(木)

C++ の演習を教えていたのだが、 訳のわからないエラーに遭遇した。
#include <cstdio>
int main()
{
  double d;
  sscanf("1","%lf",&d);
  return 0;
}

上のプログラムが、VC++6 で実行時エラーになってしまう。 原因は「浮動小数点ライブラリがリンクされていない」からであった。 例えば、sscanf の前に、d=0; とか入れておくと大丈夫となる。


00/7/12(水)

fj.comp.lang.c++ もレベルが落ちたね。 まだ、2チャンネルの方がためになるような気がする。 fj でまともな議論ができるのは数人しかいないようだ。 まともな人とレベルの低い人が議論しても、 まったくの無駄のようである。
(だから、みんな STL 使おうよ。 使いもせずに間違ったこと言わないようにしようよ。恥ずかしいから)

00/7/14(金)

MS が C# というのを発表したそうだ。 詳しく知らないが、ガベージコレクション(GC)もついているそうだ。
前にも書いたが、GC は要らない。

Java で「全てのクラス変数が ポインタ であって GC が装備されている」 ことは大失敗ではないかと思っている。
ちょっと乱暴だが、例えてみれば「Java のメモリ管理は UNIX のハードリンクに似ている」 また「C++ のメモリ管理は UNIX のシンボリックリンクに似ている」
ハードリンクはリンクされているものが全て同列になる。
% touch aaa
% ln aaa bbb
% ln aaa ccc

このようにすると、3つファイルができるが実態は1つである。すなわち、aaa を修正すれば、 bbb,ccc も修正される。しかし、3つのファイルは同列である。aaa を消しても bbb,ccc のファイルは残り、中味も残る。そして、bbb,ccc も消すと、実態も消える。 これは、Java の世界と同じである。

% touch aaa
% ln -s aaa bbb
% ln -s aaa ccc

このようにしても、3つファイルができ実態は1つである。しかし、実態は aaa そのものである。 従って、aaa を消してしまうと、bbb,ccc のリンク自体は残るが参照先がないので、 bbb,ccc の中味を見ることはできなくなる。
UNIX の世界では、シンボリックリンクの方がハードリンクより使いやすいのは常識である。 理由は、「所有」か「参照」かがはっきりしているからだ。これと同じ理由で、 Java プログラマは「所有」と「参照」の区別がつかなくなる可能性がある。 これは悲劇である。

メモリ管理に悩まされたことがあるプログラマは、GC が必要と感じるだろう。 私も、メモリ管理でわけわからなくなってプログラムを捨てざるを得なくなったことがある。 しかし、繰返すが GC は必要ない。必要ならば自分で作ればいい。 それに STL を使うと、new や delete を書かずにプログラムを書くことができる。 (なおかつ、多様性も使うこともできる)大事なのは、「所有」か「参照」かを はっきり区別することである。 コンテナに何を入れるかが 非常に参考になるだろう。(今日はまだできていないけど)
3/14 の話参照。


00/7/19(水)

ドラッグ&ドロップについて。
Windows のヘルプを読むとドラッグ&ドロップに対応するために、 COleDataSource や COleDataObject を使わないといけないようで、 面倒なのかと思っていた。で、ある所の Tips に、エクスプローラから のドラッグ&ドロップの対応方法に書いてあったが、割と簡単であった。
  • ダイアログもしくはフレームで、DragAcceptFiles()を呼ぶ。
  • 同じクラスに WM_DROPFILES のハンドラ OnDropFiles(HDROP hDropInfo) を定義する。
    // m_val にファイル名を表示する例
    void CWTestDlg::OnDropFiles(HDROP hDropInfo) 
    {
    	int		i,n;
    	char	buf[MAX_PATH+1],*p;
    	n = ::DragQueryFile(hDropInfo,0xffffffff,buf,sizeof(buf)-1);
    	CString	s;
    	for (i=0;i<n;i++) {
    		::DragQueryFile(hDropInfo,i,buf,sizeof(buf)-1);
    		if (i) s += "\r\n";
    		p = strrchr(buf,'\\');
    		s += p ? p+1 : buf;
    	}
    	::DragFinish(hDropInfo);
    	m_val = s;
    	UpdateData(FALSE);
    }
只、これだと画面全体にドロップできてしまう。特定のコントロールだけ 対応させるには以下のようになる。
  • そのコントロールの DragAcceptFiles()を呼ぶ。 ダイアログもしくはフレームでもできるが、カーソルが変化する範囲が画面全体になる。
  • コントロールのプロパティの拡張スタイルで「ドラッグドロップを許可」をON。
  • コントロールの派生クラスを作成し、WM_DROPFILES のハンドラ OnDropFiles(HDROP hDropInfo) を定義する。
  • ClassWizard でそのコントロールに作成した派生クラスの変数を作成する。
派生クラスを作成しないといけない所が面倒だけど、他にいい方法はないかな。

00/7/21(金)

ダイアログコントロールの変数の配列化について。
ダイアログ画面を作成していると、よく、何列も同じコントロールを並べたりする。 例えば、IDC_EDIT1, IDC_EDIT2, ... というような感じでエディットボックスを作ったりする。 これらを、ClassWizard で変数を追加すると、m_edit1, m_edit2, ... というようにしかできない。

このままだと不便なので、CString* m_pEdit[N]; とメンバを追加して、 ダイアログのコンストラクタで、m_pEdit[0] = &m_edit1; ... などとして 配列で使えるようにするのだが、結構、面倒である。
どうも直接、配列変数をコントロールに結びつけた方がよさそうである。 この方法では、ClassWizard は使わない(使えない)。やり方は、 メンバ変数、CString m_edit[N]; を宣言し、 DoDataExchange() で DDX_Text(pDX, IDC_EDIT1, m_edit[0]); というように書くだけである。 (ClassWizard で使うわけでないので、//{{AFX_DATA_MAP(...) の外に書くこと。)
できれば、ClassWizard が対応して欲しいものだ。


00/7/24(月)

WinZip について。
プロジェクトの最新ソースを圧縮してサーバに置くようにしている。 WinZip だと、Explorer からポップアップメニューでフォルダごと圧縮できるので、 便利である。 圧縮ファイルは、履歴を残すためにファイル名を変更していた。 例えば、圧縮したファイルが XXX.zip の場合、XXX0724.zip のようにその日の日付を追加していた。 この作業を自動化したかったのだが、どうしたらいいか困ってしまった。

圧縮は、メニュー一発でできているので、コマンドラインからもできるはずである。 しかし、WinZip のオプションがわからない。 試行錯誤を繰返すこと以下の通り。

  • WinZip のヘルプを調べる。
  • 適当なオプションをいろいろ付けて起動してみる
  • WinZip32.exe のバイナリ自体を vi で開いて、オプションらしきものを探す。
  • レジストリでそれらしいものがないか探す。
  • Windows のプロセスビューワーで調べる。
  • Cygwin32 の ps で調べる。
結局、WinZip32.exe の名前を適当に変更して、前述のメニューを実行することにより 表示されるエラーメッセージからわかった。具体的には、 「WinZip32 -% -a -r 作成ファイル 元フォルダ」であった。
後、ファイル名に日付を追加する部分は、C++ で書いて、元々のやりたいことを実現できた。

00/7/27(木)

modeNoTruncate の使い方を間違えた。
たいした話じゃないのだが、MFC の CFile で Open するとき、 modeWrite でオープンしているのに、modeNoTruncate を付けていたため、 ファイルにゴミが残ってしまった。このバグは、上書きで、前より少ない量を 書かないと気づかない。modeNoTruncate は、 Open(filename, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeRead); という形でしかまず使わないであろう。であれば、最初からこの3つを1つにして欲しかった。

00/7/28(金)

プロファイラについて。
あるアプリが遅いので、高速化しようと思った。 最初、O(N^2) の部分を O(N) になるように直してみた。 しかし、全くスピードが変わらない。そこで、プロファイラを使ってみると、 修正した所は、元々あまり時間がかかっていなかったので元に戻した。
次に、プロファイラで一番時間がかかっている所に着目した。 ファイルの読込み部分に時間がかかっていたので、一旦、ファイルの内容を メモリに持つように修正した。この結果、時間が約70%減となった。
次に時間がかかっている所を、何も考えず、inline に変えてみた。 結果は、逆に、わずかに遅くなった。よくよく見るとその関数は 呼ばれる回数が少ないので、意味がなかった。inline の鉄則を忘れていた。
「inline は短くてたくさん呼ばれる関数のみ指定する」
ちなみに、「短くてたくさん呼ばれる関数」で inline にしている所も あるのだが、これの inline を外すと時間は約50%増となった。

さらに、プロファイルをとってみると、vector の insert で時間がかかっていたが、 insert は使っていない。これは、push_back の中で呼ばれているものだった。 私は、vector でサイズが分からない時、push_back をよく使っている。 今回は、サイズが予め分かっていたのだが、reserve しておいてから、 push_back していた。この部分を、reserve を resize に直し、push_back を 使うのを止めたところ、さらに時間を約25%減らすことができた。 push_back に時間がかかるのは意外であった。g++ だとまた違うかもしれない。
(ちなみに、reserve + push_back の部分を push_back だけに直してみても、 時間は殆ど変わらなかった。これは、非常に不思議であった。)

ここまで来て、最初に修正した「O(N^2) から O(N)」の部分をもう一度修正してみた。 すると今度は、時間が約7%減となった。 一般に、高速化を行う場合プロファイリングした結果を使って行うのが妥当とされている。 しかしここで感じたのは、「例え重要でない所でも、オーダが変わるような変更は 行った方が後々よいことがあるかもしれない」ということである。

また、vector で operator[] を使用している所を反復子に変えてみたりもしたが、 殆ど変わらなかった。他にもいくつか変更を試みたが、時間の変わりがない所は 表現の簡単な方を採用した。今回、久しぶりにプロファイラを使ってみたが、 なかなか興味深かった。新人にも研修でやらせてみるかな。


00/07/30(日)

inline 関数について。
「Efficient C++」というプログラミングの本を買った。 最初、「Effective C++」のパクリかと思ってた。 ところが、かなり評価できる内容である。 細かい間違いはあるが、概ね正確で網羅的である。 内容の大半は、これまでも注意している事柄であったが、 新たに気づかされることもあった。例えば、 パフォーマンスのネックが、new や delete にあるとき、 これまではあきらめていたが、独自の定義で充分改善できることがわかった。 (もっとも、改善の主な原因は inline によっている。)

また、よく誤解されている「vector は 組込み配列より遅い」という ことについても充分な実験結果を通して、説得力のある反論を展開している。 (fj.comp.lang.c++ の参加者もみんな読んで欲しいものだ。)

この中で興味を引いたのが、inline の使い方である。 効果的な方法がいくつも紹介されている。
これまた、よく誤解されることだが、「inline 関数は、依存性を悪くするので使わないほうが良い」 ということがある。確かに、開発時に inline 関数を修正すると多数のファイルが再コンパイルされ 開発効率が悪くなる。だからといって inline 関数を使うべきでないというのは、ナンセンスである。 開発が終了したら、プロファイラを見ながら inline 化していけばいいのだ。
これをサポートするための方法も紹介されているが、難点がある。

  • 拡張子が inl というファイルを扱っている。
  • 1つのクラスに関連するファイルが *.h, *.cpp, *.inl の3つになる。
ここでは、*.inl ファイルを使わない方法を紹介する。

XXX.h:
  #ifndef HEADER_XXX_H
  #define HEADER_XXX_H
  クラス宣言
  #ifdef INLINE_IND
  #include "XXX.cpp"
  #endif // INLINE_IND
  #endif // HEADER_XXX_H

XXX.cpp
  #ifndef HEADER_XXX_H
  #include "XXX.h"
  #ifdef INLINE_IND
  #undef INLINE_IND
  #else // INLINE_IND
  #define INLINE_IND
  #endif // INLINE_IND
  非 inline 関数の定義
  #endif // HEADER_XXX_H

  #ifdef INLINE_IND
  inline 関数の定義(頭に INLINE_IND を付けること)
  #endif // INLINE_IND

inline 関数を無効にしたい時は、何も定義しない。 有効にした時は、INLINE_IND=inline を定義する。 (VC++6 では、INLINE_IND を INLINE にするとおかしくなるので注意)

もっとも、このテクニックは、単純な Make しか使えない環境では、 避けたほうが懸命であろう。しかし、VC++6 では、効果がある。 すなわち、非 inline 指定時に XXX.cpp を修正しても、コンパイルし直すのは XXX.cpp だけである。これは、VC++ が #if ディレクティブ を 理解ながらコンパイルすべきファイルを決定しているからである。 (プリコンパイル済みヘッダを使用していても問題なく使える。) しかし、inline 指定時は、コンパイルするファイルが増えてくるので、 inline 関数をヘッダに移した方がいいかもしれない。


00/07/31(月)

シリアライズのための設計時のポイントについて。
シリアライズのことを考えると、MFC は魅力的である。 (必ずしも効率的ではないが) しかし、CObject から派生させたくないこともあるだろう。 そのときは、以下のポイントを考慮したほうがいいだろう。
  • 所有と参照をはっきりさせる。
  • コンテナは STL ベースのものを利用しよう。
  • 反復子のシリアライズは、基本的に以前紹介した方法でよい。 注意すべきは、反復子の前にコンテナ本体をシリアライズする所である。
以前のプロジェクトで、string をシリアライズするために、 手抜きで、CString に変換していたことがあったが、 効率を考えるならば、string 用のシリアライズを用意しておいた方がいいだろう。

00/08/01(火)

vector の at について。
STL は一般的に効率がよい。これは、一般的な状況においてであり、 特殊な状況ではもっと効率のいい方法が存在するかもしれない。 この辺の話は、「Efficient C++」に詳しい。

STL の中でも vector は最も効率がよい。但し、メンバ関数 at は 使わない方がよい。STL から外した方がいいだろう。 理由は、非効率的だからである。添え字の範囲チェックは必要なものだが、 常に必要なものではない。同じコードでスイッチできる方がいい。 ということで、私は、ライブラリのヘッダ <vector> を以下のように修正している。
  const_reference operator[](size_type _P) const
  {
#ifdef ASSERT
    ASSERT(_P >= 0 && _P < size());
#endif
    eturn (*(begin() + _P)); }
  reference operator[](size_type _P)
  {
#ifdef ASSERT
    ASSERT(_P >= 0 && _P < size());
#endif
    return (*(begin() + _P)); }

MFC を使用していれば、ASSERT は定義済みである。MFC を使用していなければ、 自前で定義すればいいだろう。(#define ASSERT assert または、ASSERT=assert)


00/08/08(火)

マウス操作のハンドラについて。
MFC なら、OnLButtonDown, OnMouseMove, OnLButtonUp を 作ってやればよい。問題は、操作の流れが分断されるため、プログラムに フラグがたくさん出てきて分かりにくくなってしまうことである。 処理のモードが増えると、保守が大変にもなる。 以下に、解決方法を示す。
  • CXXXView に以下のメンバ、メンバ関数を追加。
      void (CXXXView::*m_handler)(UINT nFlags, CPoint point);
      void OnMouseClick(UINT nFlags, CPoint point);
      bool CheckMove(UINT& nFlags,CPoint& point);
    

  • CXXXView のコンストラクタに以下を追加。
      m_handler = &CXXXView::OnMouseClick;
    

  • CXXXView に WM_LBUTTONDOWN のハンドラを作成し、以下を追加。
      if (m_handler && !GetCapture()) {
        SetCapture();
        (this->*m_handler)(nFlags,point);
        ReleaseCapture();
      }
    

  • 以下を追加。
    bool CXXXView::CheckMove(UINT& nFlags,CPoint& point)
    {
      MSG msg;
      while (::GetMessage(&msg,0,0,0) >= 0) {
        if (msg.message == WM_MOUSEMOVE || msg.message == WM_LBUTTONUP) {
          nFlags = msg.wParam;
          point = CPoint((DWORD)msg.lParam);
          return msg.message == WM_MOUSEMOVE;
        }
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
      }
      return false;
    }

  • OnMouseClick を作成する。以下、例。
    void CXXXView::OnMouseClick(UINT nFlags, CPoint point)
    {
      int     i;
      UINT    flg;
      CPoint  pt;
      i = point 位置でのオブジェクトのID
      if (i が無効な値) {
        if (作成モード) {
          point 位置にオブジェクト追加
          Invalidate();
          return;
        } else {
          // ドラッグ中に四角を描く
          CRect     r(point,CSize(0,0));
          CClientDC dc(this);
          int pr = dc.SetROP2(R2_NOT);
          DrawRect(&dc,r);
          while (CheckMove(flg,pt)) {
            DrawRect(&dc,r);
            r.BottomRight() = pt;
            DrawRect(&dc,r);
          }
          DrawRect(&dc,r);
          dc.SetROP2(pr);
          return;
        }
      }
      while (CheckMove(flg,pt)) {
        ID == i のオブジェクトを pt に移動
        Invalidate();
      }
    }

OnMouseClick がボタンを押して、ドラッグして、離すまでの処理を行っている。 モードによって処理を切替えたければ、m_handler に違うメソッドを設定すればいい。
CheckMove がドラッグ中は座標を設定して、true を返す。マウスを離したら false を返す。

状態に応じて、処理を切替えるには、Java の MouseListener 相当の クラスを作ることもできる。もっと、簡単に enum か int のフラグで 処理することもできるだろう。どちらがいいだろうか?クラスを作った方が 柔軟ではある。結局は、どちらも一長一短があるので、便利な方を使えばいいだろう。 組合わせてもいいだろう。
とはいえ、クラス管理した方がわかりやすいケースというのは、 モードがいっぱいあるようなGUIと想像できる。使いにくそうな気がする。 ということで、上記の例ではクラスを使っていない。


00/08/09(水)

set を使ってバグってしまった。
ある機能で、項目数が足らないというバグが出た。調べてみると、 データを一旦 set に集めてから処理していた。この時、比較関数上で同一と 判断された項目が上書きで消えていたのだ。面倒なので、multiset に変更した だけでバグを修正した。本来、やりたいことはソートだけだったので、正しくは、 vector に入れて sort すべきだったと思う。

00/08/18(金)

前に護輪軌条の話を書いたがそれに関して。
会社でも「インスペクションを徹底しよう」ということになってきている。 品質を向上させるためには、作成後すぐに検査することが重要であるようだ。 (参考ソフトウェアエンジニアのためのホームページ) つまり、各工程後にインスペクションを実施するのが良いということである。 前の話は、「コーディングの工程におけるバグの混入を防ぐには」というつもり だった。(バグの混入は他の工程でも起こり得る) そういう意味では、コードレビューが効果的ということなのか。 実際、 レビューだけでテストしないという技法もある。 (この技法によれば、WindowsNTやWindows2000 は捨てた方がいいと言えるようだ)

私と言えば、かなり適当である。要求仕様書からいきなりコーディングしてしまうぐらいである。 設計といってもメモ書き程度のことが多い。
また、プログラミング能力の個人差が非常に大きいことが気になっている。 もっと工学的にやれないものだろうかと常々思っていた。 そこで、SEI(カーネギーメロン大学のソフトウェア工学研究所)のハンフリー教授の 「ソフトウェアは真の工学になりえるか?」というセミナーを聞いて、 PSP(パーソナルソフトウェアプロセス)技法 を勉強することにした。今、5章を終わった所だが、結構きつい。人にも勧めているのだが、 かなりのやる気を必要とするだろう。
ここで強調したいのは、「やる気が大切」なのではないか?ということである。
去年、入社以来 最も大規模な運用システム開発を行った。結果的には、成功したと思っているが、 工学的な手法は何ら用いていない。常に気にしていたのは「プロジェクトを成功させよう」ということだけである。
思うに、「インスペクションをやれば上手くいくだろう」ではだめだろう。 「上手くやりたい。そのためにインスペクションを利用しよう」という気構えが必要だろう。 「インスペクションなど本当に効果があるのかな?面倒だな」なんてのは、やる前から失敗とわかる。 工程は大事であるが、その前に、行う本人が完全に納得し効果を確信するように持っていくべきである。
そのためにはどうすればよいか?まず、簡単であること。(単純作業はコンピュータを利用して減らすべきである) 次に、効果がわかること。さらに、組織がフォローしていること。言うは易し、行うは...というところか。


00/08/21(月)

書き方について。
double 変数を 2で割りたいとき、d /= 2.; とは書かないで、 d *= 0.5;と書くようにしている。割り算より掛け算の方が速いと思っているからだ。 しかし、アセンブラを見てみると、どっちのコードも同じアセンブラになっていた。 (共に掛け算になっている。)
では、d /= 1.23456; はどうなるだろう?やっぱり掛け算になっている。 定数の逆数をコンパイラが計算してそれをつかっているみたいだ。なるほど。
では、定数でないときはどうだろう?今度は、ちゃんと割り算をしている。まぁしょうがないね。
整数演算でも、i *= 10; などは、4倍して足して2倍している。 最適化は、コンパイラに任せた方がいいということか。(コンパイラは例によってVC++6)

00/09/01(金)

名前のつけ方について。
プログラミング時間のかなりの割合を占めているのが、 名前を考えている時間である。 結局は適当につけてしまうのだが、悩むことが多い。
心がけていることは、英語を使うようにすることである。 (つまり、atai とかしないで value としたりするということである。) ローマ字だとプログラムが読みにくいし、幼稚に見える。 長い名前も好きでない。書くのが面倒だし、横に伸びると印刷したときに、 折り返されて読みにくいからだ。
とはいっても、長い名前にした方がよいときもある。 短すぎて意味がわからなくてはしょうがない。
クラスのメンバ関数では、クラスで性格付けされているので、短い名前でも 十分なことが多い。
例えば、CFile f; f.Open(...); などは、 「ファイルのオープン」だと見て取れる。しかし、長い名前にしておいた方がいいこともある。 よく困るのが、どこで読込みをしているのか調べようとして、 読込みの関数 ReadFile を検索すると、数十個も出て来たりするときである。 各クラスに ReadFile があるため、こんなことになったりする。このときは、名前を 変えておけば良かったと思う。(例えば、ReadFileDisplay とか)
この点、CRect のメンバ関数は見習った方がいいかもしれない。殆どのメンバ関数に Rect という単語が含まれている。「CRect のメンバ関数なのだから Rect はなくても いいんじゃないか」と思う人もいるかもしれないが、あった方がいいこともあるのだ。

00/09/13(水)

初期化の仕方について。
  int  i = 1;  // (1)
  int  i(1);   // (2)

このように初期化の方法には、2通りの方法がある。 この場合は処理は全く同じになる。私は、(1) のように書くことが多いが、 (2) のように書く人も増えている。(2) は C では書けなかったため、私は (1) のように書く習慣がついてしまっているのだ。
  A  a = 1;  // (3)
  A  a(1);   // (4)

組込み型でない場合でも、(3) と (4) は共に初期化処理となり同じである。 ((3)は代入ではない。) しかし、見た目以外に 2つの点で (3) と (4) は異なる。 1つは、引数を2つ以上に変更する場合である。
  A  a = A(1,2);  // (5)
  A  a(1,2);      // (6)

このように、(3) から (5) に変更するのは面倒である。 このときは、(6) のように書く方がいいと思う。
もう1つの違いは、A のコンストラクタに explicit がついている場合である。 このときは、(4) の書き方は許されるが、(3) は許されない。
メンバイニシャライザでの書き方も、括弧を使い、「=」は使用できない。 いろいろな書き方が混じるよりは統一すべきであろう。であれば、以上のようなこと を考えると、初期化は括弧を使用すべきと思われる。
しかし、私はこの書き方にまだ慣れない。


00/09/14(木)

反復子について。
STL(vector,list,map,set,multimap,multiset) の反復子(iterator, const_iterator)は、 どれも 0 を代入することができる。 (ちなみに、reverse_iterator や const_reverse_iterator はできない。) 規格で求められているわけではないが、私の使用しているコンパイラ (VC++6.0、gcc 2.95.2)ではどちらもそのように実装されている。
(注:反復子に求められているのは、インターフェースのみである。)

反復子に 0 を代入している時に呼ばれているのは、反復子のコンストラクタである。 (vector は除く)従って、代入だけでなく、0 と比較することも可能である。
また、呼ばれているコンストラクタで合致しているのは、実装依存の内部クラスの ポインタを引数にしているコンストラクタである。(vector は除く) これは、まずいであろう。これらのコンストラクタは、explicit を付けるべきである。
バグの原因となりやすいので、早いとこ直した方がいいと思う。 VC++ なら自分で開発環境のヘッダ list や xtree を直せばいい。 以下のプログラムは、通常ならばエラーは出ないが、直せば10箇所でエラーとなる。
#pragma warning (disable : 4786)
#include <list>
#include <map>
#include <set>
using namespace std;

int main()
{
	list<int>::iterator               i0;
	map<int,int>::iterator            i1;
	set<int>::iterator                i2;
	multimap<int,int>::iterator       i3;
	multiset<int>::iterator           i4;
	list<int>::const_iterator         i5;
	map<int,int>::const_iterator      i6;
	set<int>::const_iterator          i7;
	multimap<int,int>::const_iterator i8;
	multiset<int>::const_iterator     i9;
	i0 = 0;
	i1 = 0;
	i2 = 0;
	i3 = 0;
	i4 = 0;
	i5 = 0;
	i6 = 0;
	i7 = 0;
	i8 = 0;
	i9 = 0;
	return 0;
}

直した所(該当行のみ)
<list>
  explicit const_iterator::const_iterator(_Nodeptr _P)
  explicit iterator::iterator(_Nodeptr _P)
<xtree>
  explicit const_iterator::const_iterator(_Nodeptr _P)
  explicit iterator::iterator(_Nodeptr _P)


00/09/18(月)

初期化の話の続き。 前の話では、初期化は括弧を使おうということにしたが、 実際にやってみると、いろいろ不都合が出てきた。
  int* f();
  int* p(f());

上の様に書くと、VC++6 ではシンタックスエラーになる。「=」を使えば OK となる。 変数が、組込み型で、かつ引数が定数でないときに括弧を使うとエラーになるようだ。 g++ では、エラーにならないので、VC++ のバグだと思う。
あと、デフォルト引数の指定でも、「=」は OK だが、括弧は使えない。 これは仕様である。はっきりしているのは、初期化ではどちらか一方だけ使っては 書けないということである。しょうがない。使い分けるようにしよう。 通常は、組込み型は「=」とし、ユーザ型は括弧にするかな。


00/09/19(火)

印刷でバグった。 斜めの字を印刷するとき、斜めのフォントを作成するのだが、 何故か、プレビューと実際の印刷で角度の方向が違うのである。 しょうがないから、印刷先が、プレビューかプリンタか判断して 書いていたのだが、忘れてしまっていた。それで、後で、不可思議なコードを見つけ 修正したつもりで、バグを作ってしまっていた。(実は私ではないけど) 結局、通常と異なる処理を書いたら、ちゃんと、コメントに残しておかないとダメだね。

00/09/20(水)

いつコンパイルするかについて。 PSP(パーソナルソフトウェアプロセス)のレビューをしていて、 「コーディングとコンパイルが分かれていない方がよい」という意見があった。
PSP では、コーディング工程とコンパイル工程ははっきり分かれている。 (さらに、これらの工程の間にコードレビューが入る)
現実には、コンパイルをいつするかと言うと、コーディング中に頻繁にしている。
「これは、妥当なことなのか」
大型計算機の時代では、コンパイルは大作業であり、1日に限られた数しか できなかった。その時代であれば、完全にコーディングを終えてから コンパイルするというのは理にかなっている。
最近では、いうまでもなくコンパイルは簡単にできる。事実、多い時は 1日に数十回することもある。
特に C++ に顕著なのが、「コンパイルしてみないとわからない」というのがある。 すなわち、C++ の言語仕様があまりにも複雑なので、書いたプログラムが 正しいかどうかが見ただけでは分からないというのがある。
STL を使い始めた時に、訳分からないエラーメッセージが出て どこを直していいやら途方にくれた人も多いと思う。 慣れてくれば、どこを直せばいいかは分かるようになる。 しかし、STL を使っているとどんなに注意深く書いたとしても 一発でコンパイルが通るプログラムを書くのが、至難の技になることもある。 そのような状況では、コンパイルはもはやコーディングの一部と言ってもよいであろう。

結局、レビューでは、コンパイル工程は、コーディング工程に含めることになった。


00/09/21(木)

Outlook の話。 私は Outlook が嫌いである。理由は押し付けがましいからである。 当然、IE も嫌いである。
ある日、自宅の PC のディスク容量が足らなくなったなったので、 Outlook なんかいらんだろうと思って Uninstall した。
次の日、妻が「メーラが使えなくなった」と言ってきた。 まさかとは思ったが、Outlook を使っていたらしい。 「あれ、今まで Netscape 使ってたのに」と思ったが、 確認せずに消してしまったのは、私が悪い。 しょうがない、再インストールすることにした。
しかし、インストールできない。C ドライブに空きがないと言われる。 今まで入っていたのだから、バカな話だ。(まぁMS のやることだ。) C ドライブは元々、200MB とかぐらいしかないので、ダメそうだ。 Netscape を消して、D ドライブを空けた。しかし、C ドライブに IE5 が残っているのは不気味だったので、それも Uninstall して、 IE5 と Outlook を Dドライブに入れ直した。やれやれ、何時間もかかった。
どうにか、使えるようになったが、起動してみて驚いた。 なんと、Outlook の過去のメールもアドレス帳も全部残っている。 これはこれで良かったのだが、恐るべし Outlook。

C++ とは関係なかったな。


00/09/22(金)

再利用について

定義
「過去使用したコードを再び使用すること。再利用のための情報は更新管理ができなければいけない。」

更新管理ができないもの(例:標準ライブラリ、商用ライブラリ)は、 いわゆるライブラリでありここでは再利用とは言わない。

再利用情報は、手を加えずにそのまま利用できれば、最も利用しやすい。 フレームワークなどは、追加修正することが前提となる。
再利用される情報は、保守性が高くなっているべきである。

再利用方法 \ 領域部品
ボトムアップ
全体(フレームワーク)
トップダウン
派生
(パターン)
コントロールなどMFC のドキュメント・ビューなど
テンプレートSTL, ATL などあまりない
その他
そのまま使う
非常に多い使いやすいものは少ない

使用する領域は、部品か全体かに分かれる。基本的には部品の方が多い。全体を再利用することは 非常に困難である。全体の再利用には、VC++ の CustumWizard が便利である。 部品としてのコントロールの再利用にはコンポーネントギャラリが便利である。
再利用方法には、大きく分けると派生、テンプレート、その他に分けられる。 派生とテンプレートは独立の関係であるから、両方の方法を利用しているケースもあるだろう。 しかし、派生による再利用とテンプレートによる再利用は思想が異なっており、 一般には両方用いられることは少ない。そのまま利用できるものは、その他としているが これが最も多いであろう。しかし、そのまま利用するものは、柔軟性に欠けることが多い。
利用形態としては、サンプル集などのデータベース形式のものや、ウィザード形式のものがある。 何れにしろシステムとなっていないと、再利用の効果は半減する。

再利用データ
再利用データの更新
←―――――――――
―――――――――→
再利用
設計者

ユーザ

  • 保守性は全ての工程で考慮しなければいけない。
  • 再利用は次期プロジェクトの設計で考慮される。
再利用するためには、まず、既存の再利用データを調査しなければいけない。 次に、実際に再利用が可能であれば、これから必要な機能を使えるように修正する必要がある。 そうして始めて再利用できる。

最初から再利用可能に作成することは、非常に困難である。 可能なのは保守性を高くすること(修正が容易になるように作ること)だけである。 再利用するために、どのように設計しなければいけないかは、 再利用が必要になった段階でようやく分かるものである。


00/09/25(月)

オリンピック新記録。 日本女子陸上初金メダル。すばらしい記録にもかかわらず、 ゴールした高橋選手には、余裕が感じられた。インタビューにも、 「楽しい42kmでした」。すごいとしかいいようがない。

さて、C++ の話でもするか。C しか知らない人に C++ の参照を説明するのに、 どのようにしたらいいだろうか?
基本的にポインタに似た概念である。ポインタと比較しながら 違いを説明しようか。大きな違いは、初期化が必ず必要であり、 参照先を変更することができないというところであろうか。 派生クラスから基底クラスへの変換や、非const から const に変換できる ところは、ポインタと同じである。簡単に言えば「参照とは別名である」。

ところで、(たしか)Ruby では、変数のスコープを表すのに変数名の前に記号を つけることになっている。私も g_、s_、m_みたいなものを付けている。 これは、非常によい仕様だと思う。ついでに、参照も何か記号をつけると良かったかもしれない。 よくバグの原因になるのが、参照のつもりなのに & を付け忘れてコピーしてしまっていることがあるからだ。


00/09/26(火)

ファイルフォーマットについて。 通常のGUIアプリだと MFC のシリアライズを使う。 それ以外にデータベース的にファイルを使う時は、 自分でファイルフォーマットを決めることになる。 その場合、戦略的にテキスト形式かバイナリ形式かを選ぶ必要がある。 以前、テキストとバイナリの混在形式を使ったこともあるが、 混在形式は、使い勝手の悪いバイナリ形式ともいえるので、バイナリに含めることにする。 基本的にはテキスト形式を選ぶことが多い。テキスト形式の方が、 データが分かりやすいし、デバッグ作業も楽である。 しかし、パフォーマンスが気になる時は、バイナリ形式にすることもある。 その時も最初はテキスト形式にしておいて、安定してきたらバイナリに移行したりすることが多い。 バイナリ形式が有効なのは、クラスをそのまま読み書きする場合(浅いコピー)などであろう。 このときは、アラインメントを気にする必要があるかもしれない。 また、ファイルの一部を読み込む時、ダイレクトにその部分に到達するために、 バイナリにすることもある。

テキストファイルに限った話をすると、コメントが書けるというメリットがある。 (バイナリで書いても問題はないがあまり使うことはないだろう。そうともいえないかな。) これが、結構重要に感じられる。ファイルのデータを使用するにつれ、 変更することがあるだろう。通常では、単に変更するだけである。 構成管理をきちんとしていれば変更情報も別に記録していることもあるだろう。 しかし、記録するのに最も適しているのは、そのデータそのものである。
1次データから2次データをファイルに作成しているケースで、 2次データを変更したとする。後で、1次データと比較して変更されている のは分かったが、なぜ変更されているのか分からないことがあった。 このとき、データそのものに変更履歴や変更理由が欲しいと思った。
プログラムにも変更履歴などを書く習慣(ルール)があるところも多いであろう。 私は、基本的にプログラムはシンプルなものを好むので、コメントは極力書かない。 しかし、後で見たときに理由がわからなくなりそうなところには、やはりコメントを残すようにしている。

コメントは本来の動作に不必要なものであるが、データファイルやプログラムソースに限らず、 付けるようにしておくべきであろう。


00/09/27(水)

再利用の話で、 「ライブラリを除く」といっておきながら、例にライブラリを載せているのは、 なんか変である。まぁ、いいか。

あと、初期化の話で、「組込み型かユーザ定義型か」でわけていた。 他の方法として、コピーコンストラクタを呼ぶ場合は「=」を使い、 それ以外のコンストラクタでは括弧を使う、というのでもいいかもしれない。 ふと、思っただけでそんなに根拠はない。


00/09/28(木)

言語と開発環境について。
後輩が、Visual C++ を言語と思っていた。言語は C++ で、 VC++ は開発環境であると説明したのだが、
「じゃあ、Visual Basic は?」
と聞かれて「うっ」となってしまった。
確かに、VB は言語でもあるが、開発環境でもある。 最近、VC++ を言語と思っている人が多かったのはこういうわけか。

00/09/29(金)

ファイル管理でバグってしまった。 あるデータ(複数フォルダ、複数ファイル)を別の場所に コピーしておく機能を作成したのだが、いくつかのファイルで うまく機能していない。 Windows 用であるから、コピーするときに「ファイル名は全部大文字にしてくれ」と いわれたので、CString の MakeUpper という関数を使っていた。 これがまずかった。全角アルファベットも変換していたのである。(「a」を「A」に)
このせいで、ファイル名が正しく認識されなくなっていた。
「ううむ、toupper はそんなことしないぞ」と思ったが、そもそも toupper は全角には使えない。 とりあえず、自作することにしよう。

00/10/02(月)

グラフクラスの更新メモ。 グラフクラスを作成している。 無向グラフをベースに有向グラフを扱おうとすると、 あまり上手くいかなかった。どうも、有向グラフをベースにした方がいいようだ。 次の様に直すか。
  • 有向か無向かのフラグを持たせる。コンストラクタで設定。
  • add_arc では、そのフラグを見て、有向なら1本、無向なら2本入れる。
  • 無向グラフは、i系とo系が同じになる。(冗長だけどまぁいいか?)
i系は入ってくるアーク、o系は出て行くアーク用のメソッド。
(殆どメモ書きだな。)

00/10/10(火)

[EC++] Effective C++ を久しぶりに読んでみた。 ([EC++] は Effective C++ の話題であることを示す。)
第1版は読んでいたのだが、第2版を読んでみると結構新しくなっているようだ。 以前、読んだ時は Effective C++ に書いてあることを全部納得したと思っていたのだが、 あらためて読んでみると、納得いかないことがいくつか出てきた。何日かにわけて 納得いかないところを書いていくようにしてみよう。
Effective C++ は多くのプログラマーの成果の結晶であるから、それに楯突くのは無謀とも いえる試みである。いくつかは、玉砕するかもしれない。

さて、最初の納得いかないところ

  • オーバライドする関数は仮想にすべき(項目37)
後輩が関数オブジェクトの operator() を仮想関数にしていたので、 非仮想にするように修正させた。理由は、仮想にしている意味がないからである。 何故なら、この関数オブジェクトをポインタ(及び参照)を通して使う予定が 全くないからである。関数オブジェクトのクラスの意味付け自体ははっきりしており、 使うところは限定されている。そこで、意味もなく仮想にする理由はないであろう。

しかし、これは Effective C++ の原則に反している。Effective C++ の原則は、非常に すっきりしており説得力もある。さて、では何がいけないのだろう。 理由を考えていると、ようやく分かってきた。そもそも、関数オブジェクトを派生している ところがまずいようだ。基底クラスでは共通部分をくくり出していたのだが、 よくよく考えてみると、基底クラスは派生クラスで実装として使っているようだ。 つまり、is-a の関係でないようだ。ここを派生にしているから原則に合わなくなっているようだ。 逆に言えば、基底クラスをユーザに提供しない(公開したくない)場合は、 実装を提供することになるので、派生関係にすべきでないということだろうか。 結局、原則は正しいという結論になった。(私の負け)
言い訳がましいかもしれないが、件のクラスは後輩が作ったやつである。(その後輩はもういないけど。)

実装としての私的派生についての私の考えは、またの機会に書こう。


00/10/11(水)

[EC++] さて、第2弾は is-a 関係にしよう。
  • 公開派生は is-a であること(項目35)
おぉ、いきなりきたね。こんな真理に反対するか?
って、もちろん反対しない。当たり前のことである。 では、どこが変かというと、「中で使っている例題」が変だ。 「正方形は長方形である(数学的事実)から、長方形クラスから正方形クラスを派生させてみる。 すると、このようになっておかしいこともある。だから、両者の関係を表すモデルに 公開派生を使うのは間違いである。」と言っているが、ここが変である。

「長方形クラスから正方形クラスを派生」させても何の問題もない。 (但し、普通は派生関係にしない方がよい
では、例題のどこがおかしいのだろうか?それは、 「正方形は横幅だけ自由に変えられる長方形であるから、...」といっているところだ。 ここでもう is-a 関係が崩れている。長方形クラスに makeBigger を使用できるといっているのは、 「横幅だけ自由に変えられる長方形」といっているのと同じことである。 さて、お分かりだろうか?例題では、もともと is-a 関係にないものを公開派生にしているからおかしいのだ。

もう1つ、例題を出してみよう。 中学生は人間である。よって、中学生クラスを人間クラスから派生させてみよう。但し、 人間クラスは自由に年齢の設定ができるものとしよう。また、中学生クラスの 年齢は 13歳から 15歳までとしよう。(現実には正しくないが便宜上)
そうすると、やはりおかしなことになってしまう。これも同じく、 「年齢制限のある中学生」が「年齢の自由に設定できる人間」ではないのに、 派生させているからである。私の言いたいことは次の通り。

  • 公開派生は is-a であること。また、is-a 関係は公開派生であらわすことができる。 (但し、派生がベストかどうかは定かでない)

00/10/12(木)

[EC++] デフォルト引数とオーバライドについて
  • 派生クラスでデフォルトパラメータを変更してはいけない(項目38)
さて、これもその通りである。ただ、ここでふと思ったことは、 「基底クラスではデフォルトパラメータ使わない方がいいんじゃないの」ということである。
つまり、
struct A {
  virtual void f(int i = 0);
};

なんて書くよりは、
struct A {
  virtual void f(){f(0);}
  virtual void f(int i);
};

こう書いた方がいいんではということである。 ふと、思っただけでいつもこうしているわけではない。

もう1つ。
デフォルト引数を持つ関数ポインタなどを使うときは引数を忘れないようにしないといけない。 デフォルト引数とは、引数を省略した時にコンパイラが補完してくれる機能であり、 関数ポインタを通すと機能しない。
void f(int = 0){}
int main()
{
  void(*pf)(int) = &f;
  (*pf)();  // NG
}

このときは、bind などが有効かもしれない。


00/10/13(金)

[EC++] マクロの話
  • #define は使わないようにしよう(項目1)
今日からは、また最初の方の項目に戻ってみよう。
さて、いきなり項目1 からである。 お題は尤もである。私の強調したいのは、「#define もまだまだよく使うよ」ということである。 本の中でも「全く不要ではない」とあるから、そんなにかけ離れた話ではない。

今回は、私の良く使う例を1つあげてみる。 20ぐらいの項目毎にソートする機能を持つ GUI アプリケーションを考えてみよう。 ソートはメニューから呼び出すようにしよう。つまり、メニュー項目が 20 あるとしよう。 実際にソートする関数は、DoSort(int type) を使えるものとしよう。 私は、このハンドラを書くときいつも次のようにしている。
#define VIEW_SORT(i) void CXXXView::OnViewSort##i() {DoSort(i-1);}
VIEW_SORT(1)
VIEW_SORT(2)
VIEW_SORT(3)
...

ハンドラのプロトタイプは決まっているから、DoSort を直接呼ぶわけにはいかない。 従って、20個の関数を書かなくてはいけないが、中味はほぼ同じである。 かといってテンプレートも使えない。こういうときは、マクロが便利である。

話は変わるが、EC++ の項目1 に以下のような記述をできないコンパイラは古いと書いてあるが、 本当なのだろうか?VC++6 はまだできないけど。
struct A {
  static const int i = 0;
};

g++ ができるのは知っているが、 てっきり g++ の拡張機能だと思っていたのだが。


00/10/16(月)

[EC++] iostream の話。
  • iostream を使おう(項目2)
言っていることは尤もである。内容に反論する気はない。 しかし、私は未だに cstdio を使いつづけている。 iostream は面倒くさいのだ。

この問題は、個人で開発しているときは「好き嫌い」で済むが、 チームで開発しているときは統一しないといけない。 現在のところ、私は cstdio を使うことで統一したいと表明している。 いずれは iostream に移っていくだろうが、まだまだ先のような気もする。


00/10/17(火)

[EC++] 仮想デストラクタの話。
  • 基底クラスのデストラクタは仮想にしよう(項目14)
これも尤もである。本に対して反論はしない。 しかし、「全てのクラスのデストラクタは仮想にしよう」とか いう奴は、この項目を誤解している気がする。 どうでもいいんだけど、そういうやつは Java でもやってくれ。

00/10/18(水)

[EC++] 公開メンバの話。
  • 公開メンバはやめよう(項目20)
今回も大胆である。「公開メンバでも構わない」 と言っている人は殆ど見かけたことがない。 ( 1人いました
この話は、前にも書いた。 原則として項目 20 は正しい。しかし、私は C++ のプログラムを何万行も 書いている。クラスの設計も数百はしているだろう。 その経験から言って、「全てのメンバを非公開にする必要はない」と言える。 嘘だと思うのなら、標準ライブラリの中を探検してみるといい。 公開メンバがごろごろ見つかるだろう。

簡潔に言えば以下の通りである。

  • なるべく簡潔に書こう
  • 再設計を怖れるな
必要なさそうであれば、始めは公開メンバでもいいのだ。 問題が生じた時にまた設計しなおそう。慣れてくれば、 問題が生じそうなら始めから判るようになるだろう。

00/10/19(木)

[EC++] const の話
  • 使えるときは必ず const を使おう(項目21)
私も、基本的にこの項目は賛成である。 しかし、C++ の言語規約はあまりにも複雑だ。 よって対応できなくなることがある。(特にテンプレートと const が組み合わさったとき)
  • const を積極的に使う
  • const を積極的に使わない
最近では、このどちらもありかなと思っている。 もちろん、混ぜるのはダメである。
通常は const を積極的に使うようにしている。

00/10/20(金)

[EC++] ダウンキャストの話
  • ダウンキャストはやめよう(項目39)
これも尤もな話である。しかし、必要なこともある。 もしかしたら、設計がよくないのかもしれないが。
Java で書いたことがある人なら同意してくれるだろう。
「ダウンキャストせずにプログラムは書けない」

00/10/23(月)

[EC++] 私的派生の話
  • 私的派生はよく考えて使おう(項目42)
私の考えは、「私的派生を使う必要はない」である。 確かに、限定メンバ関数にアクセスするのには派生が必要である。 その通り、派生することには問題はない。ただ、私的派生するのがおかしいのである。
よく考えてみて欲しい。あなたは、ある実装を利用したいために私的派生しようと しているのであろう。私的派生ならば、「実装の利用」と「限定メンバ関数へのアクセス」の 両方とも実現できる。しかし、両方とも同時にできなければいけないのだろうか? クラス階層を検討してみて欲しい。「実装の利用」と「限定メンバ関数へのアクセス」は 別々のクラスですべきなのだ。そもそも、私的派生してしまったら、次の階層からは 限定メンバ関数にアクセスできない。限定メンバ関数は何のために限定にされているのであろうか?

次に、仮想関数を上書きしたい場合も派生せざるをえない。しかし、これも同じである。 私的派生はすべきでないのである。公開派生してそれをメンバに持てばいいのである。 いずれにしろ私的派生は必要ない。(ちょっと過激に言えば、C++ の言語仕様から 取り除くべきだろう)

重要なことは、「実現できるからクラスを作るのではなく、 どういう役割を期待してクラスを作るかである。」

ちなみに、私は限定メンバの使用には反対である。限定メンバ関数なら問題ない。 理由は friend と同じく、クラスに穴を開けるからである。 (公開メンバは許容しているんだけどね)


00/10/24(火)

Ruby を始めた。 Perl によく似ている。しかし、結構違うところもある。 強烈な機能が多い。既存のクラスを派生せずに そのままカスタマイズできてしまうところはすごい。 というか、やばくないかな。例えば、
  class String
    def size
      1
    end
  end
  print "test".size
これは、「1」と出力される。

プログラムを動的に作成したいときは、かなり便利かもしれない。
あとは Ruby本が判りにくい。でも、Perl よりは好きになれそうである。 Perl には美しさを感じない。Ruby は全てとは言わないが、美しさがある。 でも、今後も流行るのは Perl なんだろうね。


00/10/25(水)

Ruby の作者のまつもとさんは、 言語オタクと公言している。 「C++プログラミング所感」を書いている私もそうかもしれない。 私が使える(使えた)言語は、おそらく 20 は超えるだろう。 使えない言語(COBOL, PL/I, Ada, SmallTalk, Object-C, Object Pascal, Python 等)も リファレンスがあればどうにかなると思う。
1つの言語を知っていれば、他の言語も分かりやすいということもある。 それに、私はもともとプログラミング言語が合っていると感じる。
これまでに覚えた言語の中で最も難しいのは日本語であろう。 まぁ、それは置いておくとして、プログラミング言語に限れば、 以下の3つが挙げられる。
  • Prolog
  • PostScript
  • C++
手続き型言語に慣れてしまった頭には、Prolog は判りにくい。
PostScript(以下 PS) は、一般にページ記述言語と言われるが、れっきとした プログラミング言語である。しかし、分かりにくさでいえば、PS は最強である。 アセンブラより判りにくい。理由は、他の言語が「動詞+目的語」であるのに対し、 PS が「目的語+動詞」であるからだ。(これは日本語もそうである。)
これが、どれほど判りにくいかは、適当なアプリケーションが生成する PS を 一度解析してから判断して欲しい。暗号と変わりないことを納得してもらえると思う。

3つ目の C++ が難しい言語であることは、言語仕様書のページ数からも言える。 この続きは、また後にするか。


00/10/26(木)

C++ は複雑すぎる。 Java くらいがちょうどいいのかもしれない。 しかし、まだ Java オンリーにはしたくない。 散々けなされている C++ だが、私は嫌いなわけではない。 大学にいたときは Lisp が最も好きだったが、 今では C++ の方がよい。

Lisp はトップダウン開発に向いている。 C++ はどちらかと言えば、ボトムアップ向きだろうか。 (とはいえ、デザインパターンやフレームワークの支援を 受ければ、C++ でもトップダウン開発しやすい。) これらの向き不向きは、変数の型の制約がゆるいかきついかによってくる。 しかし、大規模プログラムには型制限のきつい方がいいと思う。
私がどちらかと言うと重視しているのは、実行スピードである。 そうでなければ、アセンブラをやったりはしなかっただろう。 Lisp をやめて C に移行したのもスピードのせいである。 同じ機能のプログラムで、Lisp は C より 2 桁は遅いであろう。
これと同じ理由で、私は Java を使う気になれない。 「Java は十分速くなった」という主張をよく見かけるが、 あるアルゴリズムを C++ から Java2 に移植したが、スピードは 1/7 になったことがある。 プログラム自体はほぼ対応しているので、アルゴリズムが変わったわけではない。
C++ のプログラムを Java に移植する時に感じたのは、 テンプレート機能が使えないことの不便さであった。 しかし、今、Ruby を使っていてテンプレート機能を欲しいとは思わない。 なくても、なんでもできるからだ。変数にどんな構造でも入れられるし、 型なぞ気にすることなく、メソッドが呼べる。 そういう意味では、Java は中途半端である。 コードに不自由さが現れてしまう。
スクリプト言語の氾濫や C# が出たことで、プログラミング言語が これからどうなっていくのかよくわからない。 C/C++ はこれからもメジャーであり続けるのか?
本音を言えば、 「日本語でプログラミングできるようになって欲しい」 そしたら、プログラマは要らなくなるか。


00/10/27(金)

また、Ruby の話だけど、 「C++/Ruby プログラミング所感」にするかな。 (Excel も入れるか)でも、面倒だからやめよう。

今まで、Ruby で作ったのは、離散系シミュレータと 会議室予約システム のCGI。どちらも大したものではない。 ここまでの感想は、やはり「リファレンス欲しい」である。 新しい本が出たようだし、また買ってみるか。
今のところ、「どっか書いてなかったかな」と 本や Web を探しすのだが、見つからない。 結局、methods や instance_methods を 見たり、ライブラリやサンプルを眺めたりして探している。
UNIX 使ってたころは、awk の小さいスクリプトをたくさん 作っていたが、Windows になってからは command.com は 使えないし、さっぱり作ってない。 フリーの Windows 用 awk とかはあるけど、awk だけが 便利なのではなく UNIX 全体として便利だったので、 どうも Windows で awk とか使う気にはならなかった。 Perl5 も本買って読んだものの、「何だこりゃ」と思って使ってない。 Ruby の感想は高級おもちゃってところかな。 ちょっとしたものなら、Ruby が便利そうである。 (前に、C++ で作った、XXX.zip を XXX1027.zip のように ファイルに日付を追加する奴とか)
C++ による OOP と Ruby による OOP は雰囲気が変わってくる。 Ruby の方が OO らしいのだろう。そこには、型は気にされない。 メソッドだけが気にされる。メソッドがなければ、例外が飛んでくる。


00/10/30(月)

再び、私的派生の話。

「C++ プログラミング所感」の読者の方からメールを貰った。 私的派生を使った方がよいという例だ。 (以下のプログラムでは、メールから引用させてもらっている)
template <class T, int N>
class MyArray {
    T data[N];
public:
    MyArray() {} // (1)
};

このような固定長の配列のクラスがあるとしよう。 (このクラスは例なので、ここまでは「クラスがありき」とする。)
問題は、T 型の配列 data を T が組込み型の場合に 0 クリアしたい。 ということである。(1) のコンストラクタでやってしまうと T がユーザ型ではまずい。T のコンストラクタの処理の前に する必要がある。
貰ったメールでの解答は
class ZeroCleared {
public:
    ZeroCleared(char* begin, char* end)
      {for ( char* p = begin; p < end; *p++ = 0) ;}
};

template <class T, int N>
class MyArray2 : private ZeroCleared {
    T data[N];
public:
    //ベースクラスのコンストラクタは派生クラスのデータメンバの
    //構築に先立って実行される。
    MyArray2() : ZeroCleared((char*)data, (char*)(data + N)) {}
};

このようにすれば、

  • data のメモリ確保
  • data の 0 クリア
  • data のコンストラクタ呼び出し
という順番で実現できる。なるほど。でも、ZeroCleared をメンバに 持ってもできる。
template <class T, int N>
class MyArray3 {
    ZeroCleared zero;
    T data[N];
public:
    MyArray3() : zero((char*)data, (char*)(data + N)) {}
};

このときの違いは、MyArray3 では、MyArray2 より、メンバ zero の分だけ サイズが大きくなってしまうことである。 ZeroCleared のサイズは 1 だけどパディングのせいで 4 とか 8 バイト無駄にするかもしれない。 確かにこのときは私的派生の方がよい。(一本取られた。)

他にいい方法はないだろうか。
template <class T, int N>
class MyArray4 {
    T data[N];
public:
    MyArray4() {}
    MyArray4(const T& t) // data を t で初期化
};

初期化させるというのであれば、このようにして、2種類の コンストラクタを用意することもできるだろう。 (自分で呼ばないといけないけど)
また、組込み型用にはテンプレートの特別バージョンを作って対応するというのもあるかもしれない。 (たくさん、作る必要があると面倒だけど)


00/10/31(火)

組込み型とユーザ型の区別について。
昨日の話の続きである。 話の目的は、「組込み型の時に 0 クリアしたい」であった。 ユーザ型では何もしなくて、組込み型ならば処理を行うということが、 可能だろうか?型を特定するのであれば、オーバロードすればできる。 では、型を特定しないままでできるか?
この解答も、件の読者の方から教えてもらった。(以下のプログラムは貰ったメールからの引用)
namespace  private_built_in_check_space {     //ヘルパ
    class POD_checker{};
    template <class T> inline
    POD_checker*& operator,(POD_checker*& pE, T&){return pE;}
};

//Plain Old Data型の引数だけをゼロクリアするテンプレート関数
template <class T>
void POD_ZeroInitialize(T& arg)
{
    using namespace private_built_in_check_space;
    POD_checker* pE = 0;
    (pE, arg) = 0;    //コンマ演算子オーバーロード!
};

さてさて、私はこのプログラムを実際に VC++6.0 と g++2.95.2 で 動かして、POD_ZeroInitialize が組込み型だけ 0 クリアしていることは 確認できた。しかし、原理は分からなかった。 考えても分からないので、原理も教えてもらった。簡単に言えば、 テンプレート引数が組込み型の時に、カンマ演算子などの2項演算子は エラーにならずに無視されるということであった。
例えば、int operator+(int,int) は定義できない。 これと同じく、T operator+(T,T) は T が int のときには、マッチしない。 (テンプレートは奥が深い)

結論:組込み型とユーザ型を区別することはできる


00/11/01(水)

メディアについて。
お客さんに納品するときのメディアとして、最近は CD-R が多い。 理由を挙げてみると、
  • 大容量
  • たいていの PC で使える
  • 信頼性が高い
  • 繰返し使え(CD-RW の場合)、コストパフォーマンスがよい
  • 嵩張らない
というのが考えられる。

小さなシステムであれば FD(フロッピーディスク)でよいが、 最近では 1.4Mb で収まらないことが多い。 (CD-R は一応 650Mb といっているが、620Mb ぐらいに見ていた方がいい。)
次に重要なのは、どこでも使えるということであろう。 この理由から MO よりは CD-R を選ぶことが多い。
次に信頼性が高いことも重要である。 FD で納品していたときは、万一のためにいつも同じものを 2つ用意していた。 FD だと数年で読めなくなる確率も高い。 (但し、CD-ROM は半永久的に持ちそうな気もするのだが、実際の耐用年数は 保管場所にもよるが 20年ぐらいだと聞いたことがある。)

次のコストパフォーマンスもかなりいい。最近、10枚 580円で買ったのだが、 1Mb が 0.1 円以下である。
次に嵩張らない。これ故、雑誌の付録になっているくらいである。
以前の納品では、1/4 inch カートリッジが主流であった。そのときのメディアは 今でも残しているが、場所を取っている。

納品だけでなく最近ではバックアップにも用いている。 まさに CD-ROM は大氾濫している。私は、CD-ROM がこんなにメジャーに なるとは思わなかった。MO の方がもっとメジャーになると予想していた。 (書き込みできるから)
しかし、この予想は完全に間違っていた。


00/11/02(木)

標準化について。
納品物を作るという作業は、プロジェクトにつきものである。 UNIX 時代は 1/4 inch カートリッジをメディアにしていた。 そこで作成に必要な手順は、
  • 1/4 inch カートリッジをデバイスに入れる
  • % make tape と打つ
というふうにしていた。例え、未コンパイルのものがあっても 「make tape」とすれば、必要なら先にコンパイルするようになっている。 このようにしておけば、担当者が変わったとしても楽である。
これは、1つの例だが、作業の標準化をすることにより、効率よく 安心して工程を進めることができる。
但し、この業界の変化は激しいので、プロセスを固定してしまわずに 常に変化に対応できるように1人1人が心がけておくべきだろう。

00/11/06(月)

コンセプトについて。
この時期、新しいプロジェクトがいくつか始まっている。 私の担当するプロジェクトもイグニッションキーをまわし始めたところである。
プロジェクトのスタートアップにあたって、コンセプトメイキングしたくなった。 (具体的にはブレインストーミング)
このアイデアは、eXtreme Programming「iモード事件」という本から来ている。 よく言われることだが、プロジェクトの成功には明確な目標を持てというのがある。 そのためには、コンセプトを作り出すとよいように思う。 XP については、なるほどと思うところもあるので、またとりあげたい。

00/11/07(火)

Ruby で作ったシミュレータについて。
M/M/1 の離散系シミュレーションを書いたのだが、 1万単位時間ぐらいだと平気なのに10万単位時間実行すると、落ちたりしていた。
(M/M/1 は、待ち行列におけるケンドールの記号。 「到着が指数分布、サービス時間が指数分布、サーバが1台」を表す)

ちなみにエラーメッセージは次のようになっていた。

./sim.rb:17:in `<=>': undefined method `<=>' for #<Proc:0x457fb40> (NameError)
        from ./sim.rb:17:in `<=>'
        from ./sim.rb:17:in `sort!'
        from ./sim.rb:17:in `sched'
        from mm1.rb:44:in `Create'
        from mm1.rb:44:in `Create'
        from mm1.rb:44:in `call'
        from ./sim.rb:23:in `start'
        from mm1.rb:56
最初は、エラーメッセージの意味を深く考えずに Ruby では、大規模なものは動かないのかと思っていた。 しかし、よくよく見ると sort! で失敗している。実行部分は次の通り。
    @stack.sort!
sort! の対象 @stack は、 [[time1,proc1],[time2,proc2], ...] というように、時刻とプロシージャのペアの配列である。 ここでようやく「time の比較で一致したとき、proc の比較演算子がないから失敗した」 ということに気づいた。(エラーメッセージを見ればその通りなのだが) そこで、sort! を次のように修正して、きちんと動くようになった。
    @stack.sort!{|i,j| i[0] <=> j[0]}
実数の実装は double だと思うけど、double の値がたまたま一致したということなんだろう。 スケジュールは指数乱数を用いているから、たまたま起こるというのは非常に珍しいような 気がするのだが、どうなのだろう。
結論:Ruby でも大量の計算で落ちることはない。
反省:エラーメッセージはよく読もう。

00/11/08(水)

さすがに毎日更新していると、ネタがないな。 (その前に仕事してるのか?って聞かれそうだが)
Shift-JIS の第1バイト目の判定は通常、次のように行う。
bool f1(int c)
{
  return c >= 0x81 && c <= 0x9F || c >= 0xE0 && c <= 0xFC;
}
しかし、意味的には同じだが次のように書いた方が速くなる。
bool f2(int c)
{
  return c >= 0x81 && (c <= 0x9F || (c >= 0xE0 && c <= 0xFC));
}
実際に適当なサンプルで計測してみると約 30% ほど短縮された。
では、アセンブラでどうなるか見てみよう。
 case f1                         case f2
00401000   mov   eax,       |   00401000   mov   eax,
        dword ptr [esp+4]   |           dword ptr [esp+4]
00401004   cmp   eax,81h    |   00401004   cmp   eax,81h
00401009   jl    00401012   |   00401009   jl    00401026
0040100B   cmp   eax,9Fh    |   0040100B   cmp   eax,9Fh
00401010   jle   00401020   |   00401010   jle   00401020
00401012   cmp   eax,0E0h   |   00401012   cmp   eax,0E0h
00401017   jl    00401026   |   00401017   jl    00401026
00401019   cmp   eax,0FCh   |   00401019   cmp   eax,0FCh
0040101E   jg    00401026   |   0040101E   jg    00401026
00401020   mov   eax,1      |   00401020   mov   eax,1
00401025   ret              |   00401025   ret
00401026   xor   eax,eax    |   00401026   xor   eax,eax
00401028   ret              |   00401028   ret
違いは1箇所だけである。「c >= 0x81」の判定に失敗した時の ジャンプ先だけである。実に単純だがいろいろ応用できるかもしれない。

00/11/09(木)

ActiveX ネタ。 VC++ で「ActiveX コントロール」をサポートすると挿入される項目。
  • stdafx.h で 「MFCの拡張部分」の下に「#include <afxdisp.h> // MFC のオートメーション クラス」
  • CXXXApp::InitInstance の一番最初に「AfxEnableControlContainer();」
但し、ダイアログベースでのみ確認。

00/11/10(金)

OR 学会誌の11月号(vol.45 no.11)の特集は、 「ヒューマンエラーからのリカバリ」である。 結構、面白い。 以下は、590ページからの引用である。

「エラーはあってはならないから、あったならそんな組織は解体だ」などと 批判されたとしても、一方でエラーゼロなど実現できない。 許されないから隠そうとすることはモラルハザードである一方、 組織としては自衛でもある。あらゆる技術には利便性だけではなくリスクがつきまとうこと を共通の認識として、悪意のない軽微なエラーなら、外部への悪影響がなく 組織としても直ちに対応に取組んでいる場合、過剰な弾劾や犯人追及は 行わないことが結果として社会全体のリスクレベル低下に効果的である可能性について 真剣に考えてみるべき時代ではないだろうか。

えらくまわりくどいが、私はこの内容に賛成である。 では、年頭の原発の2000年問題への怒りの原因は何であろうか。 それは、「口にしたことへの責任感のなさ」である。
できないことは、きちんとできない(またはしない)と言って欲しいものだ。 (とはいえ、自分にも当てはまるのだろう)


00/11/13(月)

始めてから3年過ぎた。 そろそろペースを落とすかな。
私の小島研の修論の話をしよう。 (昔話はじじくさいが、自覚している)
タイトルは「大規模線形計画問題を解く内点法のプログラム」である。 ちょうど 10年ぐらい前に Latex で 57ページ書いている。 懐かしくなったので、第0章 序章の§0.1 はじめにを引用してみよう。

0.1 はじめに

線形計画問題(LPとも略す)に関する研究は、1940年代後半に Dantzing によって Simplex 法が発明されてから活発になされてきた。しかし、 良く知られているように Simplex 法は許容領域の端点をたどっていくため、 最悪の場合、反復回数が問題のサイズの指数オーダーとなる。[29] (但し、実際にはそれほど悪いことは稀であり、大体は多項式オーダーに近い。)
一方、許容領域の端点を通らずに内点(不等式制約において、等号が成り立っていない許容点)を たどっていく方法の研究が Neumann[32] や Hoffman[18] や、Frisch[11] らからはじまった。 また、LP に関する他の研究も活発になり最悪の場合の反復回数の上限が O(n4L) になる 方法[20] が考え出された。(Lは問題サイズ。) しかし、これらの方法は理論的には重要であったが実際の計算時間では Simplex 法にたちうち できるものではなかった。
ごく最近になって、Karmarkar が反復回数が、O(nL) で全体でも、O(n3.5L) の 計算量となる射影法[19] を発表し(1984年)、実際にこの方法が Simplex 法より速いという主張をした。
その後の数年間に、実に多くの貢献が、理論的解析や実際の実行に関してこの内点法に対して行われた。 そしてこれらの様々なバリエーションは大別すると次の3つになる。
以下略

この Karmarkar 法はその後、アルゴリズム特許で世間に知られるようになった。 ついでに次節も挙げよう。

0.2 構成

本論文では Monterio and Adler[25] らによる主双対(primal-dual)内点法を 用いた内点法のプログラムの開発と計算実験を行なった。構成は大きく次の4つにわかれている。
  • 入力
  • 前処理
  • オーダーリング
  • 内点法の実行
前処理とオーダーリングは論文[1] を、アルゴリズムは論文[23] を参考にしている。 論文[23] の作者らはこのアルゴリズムを使って OB1 というプログラムを開発している。 本論文で述べるプログラムは便宜上 YODA と呼んでいる。OB1 はFortran で書かれており ソースファイルは 1Mbyte ほどである。YODA は C で書かれておりソースファイルは 60Kbyte ほどである。以下略

60Kbyte って何行だろう。覚えてない。1000行ぐらいかな。 もっと書いたような気もするけど。 機能的には、入力ファイルとして当時標準だった Lindo のフォーマットを 扱えるようになっている。また、計算部分も疎行列を fill-in(ゼロ要素が非ゼロ要素になること)が少なくなるように 扱っており、出力もラスターファイルや PostScript で結果を出力できるようになっている。 (出力された図をそのまま Latex に組入れられる) 計算部分のパラメータ数 20 より、実行プログラムのオプション数 23 の方が多い。 自分で言うのもなんだが、結構、多機能である。 計算実験でもスピード、精度ともに、前述の OB1 と互角であった。 (ちなみに、当時 OB1 は AT&T から億単位の値段で売られていたと思う。)
このプログラム YODA も今はどっかいってしまってない。ちなみに、OB1 とか YODA は 映画 STAR WARS から来ている。

C で書くと何百行になる内点法のプログラムも、Mathematica で書くと わずか 20行ほどである。当時、C プログラムの検証に Mathematica を使っていた。 このように、スクリプト言語はアルゴリズムの本質を記述するのに便利である。 (Mathematica はスクリプト言語ではないけど) Mathematica の場合、行列演算が数式そのままに書けたのが便利であった。
現在、良く使われているスクリプト言語は、文字列処理、特に正規表現が便利であり、 1つの特徴になっている。おそらく、今後も進化しより使われるのではないだろうか。


00/11/14(火)

最近、VC++ を使っていて思うのだが、 正常にコンパイル&リンク終わった後に、 ビルドすると、またコンパイルをする。 小さいプログラムを書いているときに多い。 どうやら、次のようになっているようだ。
  • プログラム a.cpp を書いて F7 を押す。
  • a.cpp が保存される。
  • a.cpp がコンパイルされ a.obj ができる。 但し、1秒以内なので a.cpp と a.obj の時刻が同じ。
確かに、マシンスペックが上がってからこのような現象が多くなった。 とりあえず、VC++ で直して欲しい。コンパイル正常終了後、 同じ時刻だったら 1秒おいて touch すればいいんだから簡単だと思うんだけど。

00/11/15(水)

Ruby によるシミュレーションの話で、 何故、同じ時刻になるのか不思議だったが 原因がわかった。
s = 0
100.times do
  i = 0
  d = 0
  loop do
    i += 1
    d = rand
    break if d == 0
  end
  s += i
end
p s / 100

上のようなプログラムで確認したところ、平均3万2千回ぐらいで、 0 が発生している。約半分のところで出るとすると、1/2^16 の確率だ。 元が short(2バイト)なのだろうか。 どうりで、10万単位時間も流すと同じ時刻が発生するわけだ。 ううむ、どうにかして欲しいなぁ。このままだと、大規模な シミュレーションには Ruby 使えませんねえ。


00/11/16(木)

UML について。 最近、周りでも UML を使うようになってきている。 実際にお客さんとの打合せの資料でも積極的に使うようにしている人もいる。
オージス総研のセミナーにも数人が参加し、 UML技術者認定制度 のブロンズは大体みんな取った。 次は、シルバーを取っていくことにしている。
でも、部全体でやっていこうとの認識はまだなく、 個人的に広がっているに過ぎない。 それほど難しく考えずに、ちょろちょろと使うようにしていければいいと思う。

00/11/17(金)

最近 C++ 以外の話が多いのは、 ネタがないからです。誰か頂戴。
っていうより、「C++ プログラミング所感」 改め「プログラミング所感」にします。(2001年予定)

さて、最近ツリー形式のエディタを使うようになったのだが、 結構便利である。 で、思ったのが、ツリー構造っていろいろな所で使えるということである。
一般的にはネットワーク構造なのかもしれないが、 現実的には、ツリーとして捉えられるものが多いと思う。 (ツリーはグラフでサイクルがない構造。 とはいえ、関連しているものを紐付けしたいことは、 よくあるので、ハイパーリンクは欲しいところである。)
例えば、プログラムソースもツリーと見て取れる。 (実際に書いてみたりもしたが、「}」の位置に違和感が残る。) (goto いっぱいのスパゲッティはツリーじゃないかもしれないけど、それは論外)
XML もそうかなぁ。まだ、よく知らないんだけど。 ドキュメント類は、ツリーが多いと思う。

C++ の汎用テンプレートツリークラスでも作るかな。時間ないけど。


00/11/20(月)

メーラとして、Eudora 使っているのだが、 なんかリソースをバカ食いして困る。 ということで、普段、メーラは起動していない。 Windows 9X を使っているのが悪いと言えばそうなんだけど。 今度から、Windows2000 にするかな。(バグの数は減ったのかな)
Linux もいいかな。

それはさておき。サーバにメールがあるかどうか確認する プログラムを作りたいと思っていた。フリーでも転がっているけど、 自分で書いてみたかったのだが、よくわからなくて調べる暇もないので ほっておいていた。
たまたま、雑誌に Ruby の記事があったので、それを参考に Ruby で自分でも作ってみた。 実に簡単である。メールがあったら、メーラが起動するようにしておいた ところ頻繁に ML が来るのですぐ起動してしまう。 それで、その ML だけ無視するように直した。 こんなのも簡単に書けてしまうから便利である。プログラムは次の通り、
require 'net/pop'

pop = Net::POP3::new('mail')
loop do
  loop do
    n = 0
    pop.start('tsutomu','XXXX'.unpack('m')[0]) do
      pop.mails.each do |i|
        n += 1 if /ruby-list/o !~ i.header
      end
    end
    break if n > 0
    sleep 300
  end
  system('C:\\Progra~1\\EUDORA\\Eudora.exe')
end

ちなみに、頻繁に来る ML は 上を見て分かるように Ruby の ML である。
とりあえず、パスワードは一瞬見てもわからないように、pack しているが、危険と言えば危険。


00/11/27(月)

VS-FlexGrid 7.0J の不具合。 パッチ

VC++ で SIS の OCX を使っていると「デバッグサブシステムの初期化エラー」とか 出たりすることがある。原因はよくわからないが、一度、直接 exe を起動すると直ったりする。


前へ 目次 次へ