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


97/11/11(月)

関係ないが、今日は1が4つ並んでいる。
リスト構造の終端をヌルにするか、先頭にするか2通りのやり方がある。
あるプロジェクトの角度のグループ化では、後者の方が圧倒的にわかりやすい。
プログラムも最初そういう風に作ったが、後からSTLを使って書き直した。
結果的にプログラムは一部わかりにくく、一部わかりやすくなったが、コードの量は減った。 保守性から見てもSTLを使った方がよさそうである。

ここから、本題。ライブラリには3つの側面、「便利」、「融通」、「効率」がある。
これらは、それぞれ相反しているように見える。
その点、STLは3つともいい線をいっていると思う。
欠点は、コンパイルが遅くなるのと、デバッグがしにくくなるところか。 標準といいながら、まだコンパイラでインターフェースが異なるのもいまいち。 できれば、vectorの範囲チェックも欲しい所だ。 (注:at でできるが、assert のようにコードを変えずにチェックしたり しなかったりを実現したい。)
また、listはvectorほど効率的でないようだ。 (基本のインターフェースをvectorで考えて、listはそれにあわせたという感じ。)
ライブラリが便利なのは当たり前だ。
しかし、C/C++では、厳密な型チェックがされるので、融通の効くサブルーチン作成は難しい。 テンプレートを使えばこそ融通性が出てくる。(薄いラッパーですめば、一番理想的だ。) そもそも、C/C++では、コンテナのサポートが貧弱すぎる。 言語でサポートしているのは配列だけだ。 (ここで、コンテナといっているのは、配列やリストなど。)
C++でコンテナを抽象化しようとしたら、コンテンツをポインタにするよりない。 これからのプログラミング言語では、コンテナをよりサポートすることが必須であろう。 C++はSTLでCよりは進んだが、まだまだである。 Lisp並みに型フリーならばよいかどうかはわからないが。

バグの記録の方はさっぱり進んでいない。バグが出ているのは出ているのだが。


97/11/14(金)

comp.lang.c++で見つけたVC++5のバグ
#include <string>
using namespace std;
int main()
{
	string b1,b2;
	b1 = "1234567890123456789012345678901234567890";
	b2 = b1;
	b1 = "ok";
	return 0;
}
これは、実行エラーになる。困ったものだ。

98/7/24(金)

VC++のSTLのバグ
#include <iostream>
#include <deque>

using namespace std;

struct A {
  char s[256];
};

int main()
{
  int i,j;
  deque<A> d;
  deque<A>::iterator it;

  for (i=0;i<100;++i) {
    d.push_back(A());
    it = d.begin();
    for (j=0;j<d.size();++j) ++it;
    if (it != d.end())
      cout << d.size() << " not match!\n";
  }
  return 0;
}
これをみると、サイズが 8+16n のときにおかしくなる。
dequeは使えんってことか?

98/7/27(月)

64ビット整数のビット数を求める関数を フィンローダのあっぱれご意見番を参考に作った。
int NumOfBits(__int64 i)
{
	i = (i & 0x5555555555555555) + (i >> 1 & 0x5555555555555555);
	i = (i & 0x3333333333333333) + (i >> 2 & 0x3333333333333333);
	i = ((i >> 4) + i) & 0x0f0f0f0f0f0f0f0f;
	long j = (long)(i + (i >> 32));
	j += j >> 16;
	return (int)(j + (j >> 8)) & 0xff;
}
話はかわるが、CGIでページ毎の読まれる回数を 調べるものを作った。結果、読まれているのは、 「赤ちゃん」と「リンク」だけだった。
リンクはブックマークをちょっと整理したもの がメインだ。(整理しきれてないが。)
「つれづれ」は全く読まれてないようだ。 (まあ、更新を付けているのは「赤ちゃん」だけだし。)

98/7/28(月)

昨日の関数の高速化を図っていたのだが、 不思議な現象になった。
2つの関数の速度が、inline にすると逆転するのだ。 いろいろ調べてみると、最適化で計算が省かれていたらしい。
res = func();
で、変数 res を volatile にしたら予想通りになった。
うーむ、他のやつは、volatile にしてたのになぁ。
結局、インラインの効果はどの関数でも同じになったが、 場合によっては、展開後に最適化されてさらに速くなるケースも あるかもしれない。

高速化は表引きにしたら、昨日のやつの2、3倍速くなった。 表は1バイト分(=256個)持った。

ちょっと気づいたこと。
普通、for 文とか static 変数があると、inline 展開されないが、 VCではされてしまう。(便利ではあるが)
あと、MMX Pentium でPentium Pro の指定をしたら逆に遅くなった。
さらに、最適化でカスタマイズを選んで、最大の最適化を選んだのに 逆に遅くなった。


98/8/5(水)

今週は夏休みだけど、なんで書いてるかって?
(まあ細かいことは気にしないで)
さて、本題。
プログラムである部分の実行速度を知りたいとき、
	for (i=0;i<100000000;++i)
		func();
などとする。しかし、コンパイラの最適化で、
	func();
	for (i=0;i<100000000;++i);
となったりする。(あるいは、forそのものもなくなるかも)
そこで、
	volatile int i,v;
	for (i=0;i<100000000;++i)
		v = func();
と書いたりする。これでも、
	tmp = func();
	for (i=0;i<100000000;++i)
		v = tmp;
となるかもしれない。
こうなったら、吐き出されるアセンブラを見るしかない。

実際、

	k = a[i]+a[j]+a[k];  (A)
	k = 1;               (B)
(A)を(B)に変更してもスピードが全く変わらないことがあった。
アセンブラを見たら、和の計算がforの外に出されていて、 forの中では、レジスタに1つの値を代入しているだけ ということがあった。

ちなみに、アセンブラを吐き出させるには、 コンパイラオプションで指定すればよい。
(VC++では、「プログラマーズガイド」の 「コンパイルとリンク」の「詳細」の 「コンパイラリファレンス」に出ている。)
といいながら、実はデバッガで見ている。
VC++のアセンブラはMASMというやつらしいが、 見るとなんとなく動きはわかる。
(もっとも、昔アセンブラを結構やっていたせい もあるだろう。)


98/8/6(木)

前回(8/5)の補足である。
吐き出されるアセンブラは、 デバッグバージョンとリリースバージョンで 異なる。当然、リリースバージョンで見るべきである。 なぜなら、デバッグバージョンでは、行に対応した アセンブラを吐くが、リリースバージョンでは 最適化の結果、ちゃんと行とは対応しないからだ。
(デバッグバージョンの方が、わかりやすいけど)

ところで、次の2つはどちらが、実行速度が速いか?

  • ケース A
        for (it=v.begin();it!=v.end();it++)
            *it = i;
    
  • ケース B
        for (j=0;j<100;++j)
            v[j] = i;
    
VC++5 で適当に計測すると、A が 1.26 秒、B が 2.42 秒であった。
ちなみに、アセンブラを見ると次のようになる。
(コロン以降は私の注釈。edx等はレジスタ。)

ケース A
00401057   mov         edx,dword ptr [esp+8]       : edx = i
0040105B   mov         dword ptr [eax],edx         : *it = edx
0040105D   add         eax,4                       : it += 4
00401060   cmp         eax,esi                     : ? it , v.end()
00401062   jne         00401057                    : if !=0 goto

ケース B
0040105C   mov         edx,dword ptr [esp+10h]     : edx = i
00401060   mov         ecx,dword ptr [esp+0Ch]     : ecx = j
00401064   mov         dword ptr [esi+ecx*4],edx   : v[ecx] = edx
00401067   mov         edx,dword ptr [esp+0Ch]     : edx = j
0040106B   inc         edx                         : ++edx
0040106C   mov         dword ptr [esp+0Ch],edx     : j = edx
00401070   cmp         dword ptr [esp+0Ch],edi     : ? j , 100
00401074   jl          0040105C                    : if < goto

ケース B で無駄な代入が見られるが、しょうがないのだろう。
結局、配列は添字よりポインタの方が速いようである。

(9/13/99追記:添字とポインタのどちらが速いかは一概に言えないようである。 実際、v[j] = j を int* p=v.begin();(ループ外) *(p+j) = i; に変えると、 スピードは変らない。ちなみに、v[j]の定義は、*(v.begin()+j) であり、 v.begin() は、v._First である。何でスピードが変るかは不明。)


98/8/13(木)

C++ の話じゃなくてExcel97 の話である。
新規作成で、Excel5.0/95 形式で保存する。
当然、問題はない。次に、シートの左上をクリックして、 全選択して、保存してみよう。怒られるはずだ。
選択状態も保存されるのだが、これが、Excel95とExcel97で 異なるらしい。それにしても訳分からんメッセージである。

98/8/14(金)

K&R では、strcpy が次のように紹介されているらしい。 (数理にK&Rなかったっけ?)
char* strcpy(char *dst, const char *src)
{
    while (*dst++ = *src++) ;
}
初心者には、ちょっと間違いやすいかもしれない。

いろいろ、書いてみてアセンブラがどうなっているか比較してみた。 コンパイラはVC++5 である。 (最近、アセンブラに凝ってしまった。)

Case 1:      while (*dst++ = *src++) ;
Case 2:      while (*s) *s++ = *t++;
             *s = 0;
Case 3:      do *s = *t++; while (*s++);
Case 4:      while (1) {*s = *t++; if (*s++) break;}
で、結果は次の通り。
Case 1:
00401000   mov         edx,dword ptr [esp+8]   : edx = t
00401004   mov         ecx,dword ptr [esp+4]   : ecx = s
00401008   inc         ecx                     : ++ecx
00401009   mov         al,byte ptr [edx]       : al = *edx
0040100B   inc         edx                     : ++edx
0040100C   mov         byte ptr [ecx-1],al     : *(ecx-1) = al
0040100F   test        al,al                   : ? al
00401011   je          0040101D                : if ==0 goto
00401013   mov         al,byte ptr [edx]       : al = *edx
00401015   mov         byte ptr [ecx],al       : *ecx = al
00401017   inc         ecx                     : ++ecx
00401018   inc         edx                     : ++edx
00401019   test        al,al                   : ? al
0040101B   jne         00401013                : if !=0 goto
0040101D   ret

Case 2:
00401000   mov         eax,dword ptr [esp+4]   : eax = s
00401004   cmp         byte ptr [eax],0        : ? *eax,0
00401007   je          0040101A                : if ==0 goto
00401009   mov         ecx,dword ptr [esp+8]   : ecx = t
0040100D   mov         dl,byte ptr [ecx]       : dl = *ecx
0040100F   mov         byte ptr [eax],dl       : *eax = dl
00401011   mov         dl,byte ptr [eax+1]     : dl = *(eax+1)
00401014   inc         eax                     : ++eax
00401015   inc         ecx                     : ++ecx
00401016   test        dl,dl                   : ? dl
00401018   jne         0040100D                : if !=0 goto
0040101A   mov         byte ptr [eax],0        : *eax = 0
0040101D   ret

Case 3: 及び Case 4:
00401000   mov         ecx,dword ptr [esp+8]   : ecx = t
00401004   mov         eax,dword ptr [esp+4]   : eax = s
00401008   mov         dl,byte ptr [ecx]       : dl = *ecx
0040100A   inc         ecx                     : ++ecx
0040100B   mov         byte ptr [eax],dl       : *eax = dl
0040100D   inc         eax                     : ++eax
0040100E   test        dl,dl                   : ? dl
00401010   jne         00401008                : if !=0 goto
00401012   ret
こんなものたくさん出されても、わからないって?

簡単に言えば、一番すっきりしているのは、Case 3と4 となっている。 Case 1は、ちょっと余計なものがあるが、効率は Case 3と変わらない。 Case 2は、効率が悪い。という所である。

でも、素直に書いたら、Case 2だよな。Case 1とか3とか4 は、あまり書かないと思う。


98/8/17(月)

最近、VC で STL の map を使うので、
#pragma warning(disable:4786)
と書くことが多い。
(解説:VC とは、Microsoft(R)の発売する、 C,C++用開発ツールの Visual C++ のことである。 ちなみに、VC でまともに STL が使えるように なったのは、Version 5.0 からである。
また、STL とは、標準テンプレートライブラリ (Standard Template Library)のことであり、 1996年ぐらいから、C++ で使えるようになった。 もともとは、HewlettPackard で開発されたが、 C++の標準化委員会で標準ライブラリで採用された。 便利ではあるが、もっと追加して欲しい機能もある。
map とは集合を管理するコンテナの一種で、 キーと値を組で持つ辞書の様なものである。 使い所によっては非常に便利である。
解説が長くなってしまった。)

pragma により汎用的なプログラミングが 可能になっている。
Simple & Flexible のよい例といってもよいかもしれない。 それより、VC の実装者に言いたいのは、
「255文字を超えるようなクラス名なんか作るなよ」
ということである。
(解説:最初に示した pragma は、クラス名が255文字を 超えても warning を出さないように指示するものである。 map を使うと必ず255文字を超える。)(注:そうとも限らないようだ。)

STL のエラーメッセージは判りにくいものが多いが、 最近は慣れつつある。コツをお教えしよう。
「正しく書く」
あるいは、少しづつ書いていってコンパイルしていけば、 間違ったことを書いたときに、エラーが出るから分かるであろう。 STL については、C++OnePoint に多少説明を書いた。

(9/13/99追記:VC++6から上記の pragma はヘッダの前に書かないと 効果がないので注意して欲しい。)


98/8/24(月)

MS に対するぐちである。別にC++の話じゃないけど、 雑記しているのが、これだけなので、ここに書いている。

ZDnet ニュースの MS のソース利用者狩りという記事によると、 MS の開発しているアプリの中には、他のベンダに公開していない API を利用しているそうだ。なんとも人を馬鹿にしている話だ。 いくら大会社とはいえ、信用なんかできるものではない。 (もっとも、そのうち、誰かに全APIを暴露されちゃうかもね。)

もっと腹が立つのは、Javaに対しても独自機能を隠していれようと していることである。
言いたいことはいろいろあるが、とりあえず、「MS はJavaを使うな!」 Visula J++ を使うのは皆さんやめましょう。 (もし使っている人がいたら、私は品性を疑う。)


98/8/31(月)

const の話題を1つ。
(3連続月曜更新。何故だろう?実を言えば、家でも更新できるようになっているのだが。)

さて、

    const n = 10;
    int a[n];
というのは、OKだが、
    int   i = 10;
    const n = i;
    int a[n];
これは駄目である。但し、怒られるのは int a[n]; の所だ。 要するに、配列のサイズは定数でなければいけないということだ。 それで、const 指定は、定数という意味ではなく、 変更不可という意味でしかない。但し、定数で初期化した const 変数は 定数として扱えるということだろう。 この「但し...」のことは、ちょっと混乱しやすいような気がする。 まあ、使うときはそんなに気にしなくても問題ないのだが。

10/19の話も参照。


98/9/4(金)

またまた Excel の話だ。(タイトルを改名しようか。)
Excel は MS の製品にしては、評判がいいし、私もそう思ってたけど、 ポロポロ変な所が出てくる。

まず、1つめ。
ソートすると結果がおかしい。さんざん調べてやっと分かった。 (数字が左詰で表示されている時点でおかしいことに 気づけばよかった。)
表示形式を「標準」にすると、右詰と左詰に別れるのだ。つまり、 数字と文字に認識されているものが混じっていることになる。 これでは、ソートがおかしくなっていても不思議ではない。 客から貰ったデータなので、何故そうなっているのかは不明である。 とにかく、1列追加し、おかしい所を「=value(...)」でコピーし、 さらに、それをコピーして「形式を選択して貼付け」で値のみにして ペーストして一件落着。ちゃんとソートできるようになった。

もう1つは、同じデータをCSVにして保存すると、 そのファイルが開けないというものだ。

「SYLK:ファイル形式が正しくありません」

と怒られる。 これも、しばらく調べて原因は分かった。 最初のセルに「ID」と入っていたためだ。
これは、簡単に再現できる。Excelで新規作成して、セルA1に「ID4」と 入れてCSVで保存してみよう。開けないはずだ。

原因は、適当なSYLKファイルを作成して、テキストエディタで見てみれば分かる。 最初が、「ID;」で始まっているのだ。つまり、Excelは「ID」で始まっていると SYLKだと思うのだ。お馬鹿な仕様としかいいようがない。


98/9/7(月)

「見える/見えない」、「使える/使えない」の話。
private/protected/public は、アクセス制御レベルの設定である。 これは、使える/使えないの違いであって、全て見えてはいるのである。 見えているというのは、名前が同じだと困るということである。 例えば、
    class A {
        int a;
    };
このとき、メンバ関数以外でメンバ a にアクセスして、コンパイルしてみよう。 「a がない」ではなく 「a にアクセスできない」と怒られるはずだ。
この区別が必要となるのは、「同じ名前をつけそうになったとき」である。 「staic 関数の場合のファイルスコープ外」のように見えないのであれば、 同じ名前をつけても一向に構わない。

もう1つ混乱しやすいことがある。入れ子クラスでは、アクセス指定をクラス名と クラス内のメンバ(及びメンバ関数)に設定できる。
このとき、クラス名を非公開にすれば、メンバが公開であってもアクセスできない と思うのは間違いである。(このような勘違いをしているライブラリを見かけたことがある。) 例えば、

    class A {
        struct X { void Func(){} };
        X	m_x;
    public:
        X* GetX(){return &m_x;}
    };
となってたら、
    A  a;
    a.GetX()->Func();
と呼び出しても何の問題もない。 つまり、ユーザはクラス A::X は使えないのに、A::X の Func は使えるのだ。 これは、クラス設計者の意図したところではないだろう。

98/9/16(水)

Netscape4.05 で HTML を保存すると、改行が 「0x0d,0x0d,0x0a」となる。(Windows95の場合)
これは、ファイルに保存するときに、バイナリにしていないせいである。 テキストモードで保存しているので、元データが「0x0d,0x0a」(DOSでの改行コード)のように 来たときに、0x0dで0x0dを書き、0x0aで0x0d,0x0aを書いてしまっているのだ。 (NetscapeはUNIXで開発していて、Windows上ではあまりテストしてないということかな。)

98/9/19(土)

オブジェクト指向について。 (以下の話をつらつら考えてたのは、土曜日だと思うけど、書いているのは、実は月曜日だったりする。) これは、C++OnePointでも書いたけど、 書き足りなかった分かな。

小さいプログラムでは、オブジェクト指向の方が効率が悪い。何故なら、 オブジェクト指向風に書くとコードが1.5倍から2倍ぐらいに増えるからだ。
オブジェクト指向が真価を発揮するのは、大きなプログラムだ。 何故なら、問題を小さく切り分けて考えやすくできるからである。 この問題対象を切り分けることが大事なのだ。 適切に切り分けられたなら、問題は抽象的に扱うことができる。 抽象的に扱うことによって、見通しがよくなる。

デザインパターンは切り分ける場所を示し、その名前を与えるものである。 つまり、プログラミングテクニックではなく、切り分けかたのサンプル集 なのである。(唯一、テクニックに近いものは、Decorator だと思うが、 他にもあるかも。)

オブジェクト指向に関する私の助言はこうだ。

  • まず、動くものを作れ。
  • 再利用しやすくしろ。

98/9/21(月)

Pro- について。(C++の話じゃないけど)
プログラマというと、なんとなくカッコよく聞こえると思う。 たぶん、響きからプロフェッショナルとかプロフェッサ(CPUじゃないよ)のイメージが あるのかもしれない。もっとも、pro で始まる言葉は多い。 UNIXなら次のように確認できるだろう。
    grep "^[P|p]ro" /usr/dict/words | wc
全体の1%近くもある。pro- という接頭語が、「前の」とか「賛成の」 という意味があるようだから、結構いい意味のが多いかもしれない。

それはともかく、ソフト業界では、プログラマーといったら詳細設計書を元に プログラムを書くだけの人のことを指したりする。この作業は、それほど 面白いものでもなく、難しいものでもない。
プログラミングの面白さは、設計する所にあるからだ。 (といっても、プログラマーが設計も担当することが多いのも事実である。) プログラミングだけでなく、上流工程の作業もする職種をSE(システム・エンジニア) といったりもするが、呼び方が変わってもやっていることはたいして違わないと思う。

プログラマには向き・不向きがある。これは、プログラミングが好きか嫌いか とかなり相関がある。(好きならば上達していくはずだからだ。) 好きなだけでなく、論理的思考ができなければいけない。 (これができないと、プログラマとしてやって行くのはつらいと思う。) あと必要な素質は、好奇心旺盛なことだと思う。 コンピュータの業界は、次々と新技術が登場する。いつまでも古い技術で やっていたら、誰にも相手にされなくなる可能性が高い。
もっとも、変わらない技術もある筈だ。それは、しっかりと身につけた方がよいだろう。 (例えば、「プログラミング書法」とかかな。)


98/10/5(月)

VC++のヘルプはWebBrowserコントロールを使っている。 また、IE(インターネットエクスプローラ)も 同じコントロールを使っている。しかし、何故に VC++はIEを必要とするのだろうか?

(もし、私のプログラムが math.h の sin を使っていて、あなたのプログラムが、 同じく sin を使っているからといって、あなたが私のプログラムを必要になったら?)

これは、MS の陰謀としか言えない。私は、このために VC++5 を少なくとも3回は インストールするはめになった。全く、何とかして欲しいね。


98/10/6(火)

auto_ptr を誤解していた。 GC(ガベージコレクション)の代わりになるかと思っていたら、 単に、所有権が移るだけらしい。(まあ、それでも使いではあるけど。)
GC の代わりにするためには、所有権ではなく参照数を覚えておかないとだめだろう。 (GC の構築方法は他にもあるけど。) そこで、参照数によってメモリ管理する汎用のクラス refer_ptr を作ってみた。 こんな風に使える。
    refer_ptr<A>	p(new A(123)),q = p;
    p = refer_ptr<A>(new A(789));
簡単に言ってしまえば、auto_ptr のように使えるが、ちゃんとメモリ管理してくれるものである。 但し、これは配列に使えない。メモリの解放が、delete[] ではなく、delete だからだ。 では、delete[] 用にもう1つクラスを作るべきか?
いやいや、もっとよい方法がある。 それが、array_ptr というラッパクラスである。次のように使える。
    array_ptr<char>	p(new char[10]);
    delete p;	// 内部のポインタに対しては、delete[] が呼ばれる
こうすると、auto_ptr でも refer_ptr でも配列に対して使えるようになる。例えば、こんな風に
    auto_ptr<array_ptr<char> >	p(array_ptr<char>(new char[10]));
    ... // p を使用
    // ブロックを抜けると自動的に delete[] が呼ばれる
これらのラッパクラスの詳細をチャレンジの解答にのせる。 もっとも、2時間くらいで適当に作ったやつだからバグがあるかもしれない。

98/10/13(火)

strtok を誤解していた。 "123,,45" というのを "," で読込んだら、 "123","","45" と取れると思ったら、"123","45" となる。使えん。 結局、CSV データを1つづつ読むやつを自分で作った。
char* GetCSV(char** pBuf)
{
	char	*p = *pBuf,*q = p;
	if (!p) return p;
	if (*p == '"') {
		*p++ = 0;
		do ++q; while (*q && *q != '"');
		if (*q == '"') *q++ = 0;
	}
	if ((*pBuf = strchr(q,',')) != 0) *(*pBuf)++ = 0;
	return p;
}
ちゃんと、""にも対応してある。それに C でも動く。

98/10/19(月)

VC++ と g++ は同じ C++ と思いきや、結構違う。 おそらく、g++ の方が独自の拡張を入れていることが多いのだろう。

例えば、8/31 のconst の話では、VC++ では怒られるコードが g++ では平気だったりする。他にも、

    struct A {
        static i = 1;
    };
これも、VC++ では怒られるが、g++ では通ってしまう。 (怒る方が正しいと思う。)

98/10/23(金)

1byte と 2byte が混在しているときの比較の話である。
半角スペースと全角スペースのどちらも読み飛ばしたいときに、 次のように書いていた。
    while (true) {
        if (*p == ' ') ++p;
        else if (!strncmp(p," ",2)) p += 2;
        else break;
    }
でも、両方とも半角だったら、単純にこう書ける。
    while (*p == ' ' || *p == ' ') ++p;
そこで、はたと気づいた。こう書けばいいのだ。
    while (*p == ' ' || !strncmp(p," ",2) && ++p) ++p;
テストしてみると、うまく行っている。しかし、ぱっと見てよく分からないかもしれない。 でも、私は行数が少ない方が好きなのでこれを採用しよう。
次に、半角スペースと全角スペースのどちらかがくるまで読み飛ばすやつを
     while (*p && *p != ' ' && *p != " "[0]) ++p;
と書いていた。正しくは、
     while (*p && *p != ' ' && strncmp(p," ",2)) ++p;
こう書くべきだろう。(今回も C++ じゃなく C の話だった。)

追記:DOS od を見てて何か変だと思ったら、バイトオーダが UNIX と逆だからだった。
2000/4/5の話も参照。


98/10/28(水)

C++ のキャストが新しくなった。次の4種類だ。
  • dynamic_cast
  • static_cast
  • const_cast
  • reinterpret_cast
下に行くほど危険になる。dynamic_cast は、ダウンキャスト (派生クラスのポインタへのキャスト)を安全に行うもの。 static_cast は、これまでのキャストと同じ。 (なるべく安全に行おうとする。)const_cast は、const 属性や volatile 属性を追加したり、削除したりするもの。 reinterpret_cast は、何も考えずにそのままキャストするものである。 どういう風に違うかは、下の例を参照のこと。 但し、VC++5.0 では違いがでるが、g++ ではダメであった。
#include <cstdio>

struct A {
	int i;
	A(int i0) : i(i0){}
};
struct B {
	int j;
	B(int j0) : j(j0){}};
struct C : A,B {
	C(int i0,int j0) : A(i0),B(j0){}
};

int main()
{
	B	b(-1);
	C	c1(1,2);
	static_cast<B&>(c1) = b;
	printf("%d %d\n",c1.i,c1.j);
	C	c2(1,2);
	reinterpret_cast<B&>(c2) = b;
	printf("%d %d\n",c2.i,c2.j);
	return 0;
}
結果は、static_cast が C の中の B の部分を置き換えるのに対し、 reinterpret_cast は、C の中の A の部分を置き換えてしまう。 これは、C が A,B の順に並んでいるのに、C の先頭を B の先頭と 解釈するからである。

11/9の話も参照。


98/11/5(木)

論理値を表現するために bool という型がある。 bool から int に変換すると、 0 または 1 になることが、C++ の規格で定められている。 しかし、VC++ はそのようにはなっていない。 例えば、次のやつを実行すると、
#include <cstdio>
struct A {bool b;};
int main()
{
	bool	a = A().b, b = true;
	if (a && b) printf("a && b\n");
	if (a &  b) printf("a &  b\n");
	return 0;
}
「a && b」しか出なかったりする。 (実際には、初期化していない値を参照しているので結果は不明である。) 何故、このようになるかといえば、a を int にすると1以外の数(例えば2)に なり、2&1 が 0 になり、false と判断されてしまうからである。
結論を言えば、「bool から int への暗黙の変換を使用すべきではない」 ということである。(ということで、ビット演算子は整数に変換されてから適用されるから、 bool に対してビット演算子は使うべきではない。)
例えば、i = (j > 0) * 2; というのは、i = j > 0 ? 2 : 0; と書くべきである。

ついでに言えば、int から bool への暗黙の変換もない方が言語としては 親切だと思う。(そして、if 文は bool しか受け付けなくするわけだ。)
P.S. Java ではそういう風になっている。確かにこの方がよい。

(9/13/99追記:上の例は、未定義値を使用しているので、例として相応しくない。 また、上記の件も再現性がないので、バグかどうかは分からない。)


98/11/6(金)

プログラミングの本には、よく「10項目の指針」 というのがついてたりする。 その中に、「コンパイラの警告レベルは最大にしておこう」というのがあった。 はっきり言って、そういう事を言う輩は、本当にプログラムを書いているか 疑問を持ちたくなる。実際の所、適当なサンプルで警告レベルを最大にして コンパイルすると、警告が100以上も出てくる。 標準ライブラリのヘッダーの中とかでだ。 これでは、仕事にならない。

98/11/8(日)

MatheMagic という数学の先生の作った、WebPage に こんなパズルがあった。
    1      2         o
   2 3    5 3       o o
         6 1 4     o o o
                  o o o o
                 o o o o o
上の図で、「o の中に数字を入れよ」という問題だ。
どういう規則になっているかは、自分で考えてみよう。 で、規則は分かっても答は分からないので、プログラムを 書いてみた。最初は、STL で next_permutation を使ったのだが、 さすがに、15! 通りもやると終わらなかった。そこで、次のように直した。
#include <cstdio>
#include <algorithm>
using namespace std;

void Check(int n,bool chk[],int res[],int pos,int i,int j)
{
	int		k,a,b,c;
	if (pos == n) {
		for (k=0;k<n;++k) printf("%d ",res[k]);
		printf("\n");
		return;
	}
	if (j == 0) {
		if (++j > i) ++i,j=0;
		for (k=1;k<=n;++k) if (chk[k]) {
			chk[k] = false;
			res[pos] = k;
			Check(n,chk,res,pos+1,i,j);
			chk[k] = true;
		}
	} else {
		a = res[pos-1];
		b = res[pos-i-1];
		if (++j > i) ++i,j=0;
		k = a+b;
		for (c=0;c<2;++c) {
			if (chk[k] && k >= 1 && k <= n) {
				chk[k] = false;
				res[pos] = k;
				Check(n,chk,res,pos+1,i,j);
				chk[k] = true;
			}
			k = a-b;
		}
	}
}
int main()
{
	const	max = 100;
	int		res[max], n = 15;
	bool	chk[max+1];
	fill(chk,chk+max+1,true);
	Check(n,chk,res,0,0,0);
	return 0;
}
わざわざ載せるほどのものでもないんだけど、これなら一瞬で答が出る。 でも、「計算仮面」は 1分ちょっとで答をだしちゃうんだよな。簡単に解く方法でもあるのだろうか?
ここでの教訓
    「組合せをしらみつぶしに探す場合でも、うまくやれば早くできる」

98/11/9(月)

10/28のキャストの話の続きだ。
static_cast と reinterpret_cast はそれぞれ次のようにも書ける。
    *(B*)&c1 = b;          // または (B&)c1 = b;
    *(B*)(void*)&c2 = b;
結局、これがそれぞれのキャストの意味を表しているといえる。 1番目と2番目のキャストの振舞いの違いを理解できないとしたら、 それは以前の私と同じだ(安心しよう)。
たぶん、C++ を知らない C プログラマにとっては同じ意味だと思う。 C++ では、継承関係が出てきたので、
「アドレスはそのポインタ型と共に意味を持つ」
ようになったのだ。 別の言い方をすると、
「アドレスを継承関係のある型のポインタ型にキャストするとずれることがある」
ということだ。

98/11/12(木)

クラスとして const を実装するには、どうすればよいだろう。 そもそも、const 属性用に別のクラスを用意しなければいけない ケースとはどのようなものであろうか。例えば、
    iterator と const_iterator
などが考えられる。また、動的継承でも必要になるかもしれない。

では、const 属性が付いたときの振舞いはどのようになるだろうか。 実験してみよう。

    Any        admy;
    const Any  cdmy;
    Any        a = cdmy;
    const Any  c = admy;
    Any        aa(cdmy);
    const Any  cc(admy);
    a = c;
    c = a;                   // NG
    Any*       padmy;
    const Any* pcdmy;
    Any*       pa = pcdmy;   // NG
    const Any* pc = padmy;
    pa = pc;                 // NG
    pc = pa;
上で、NG と書いた所がコンパイルエラーになるところだ。
さて、いきなり解答だ。次のようにして、const Any を const_Any に置き換えれば、上と同じ振舞いになる。 (但し、エラーメッセージまでは同じではない。)
class Any;
class const_Any {
    void operator=(const Any&);  // 宣言のみ
public:
    // const メンバ関数
};
class Any : public const_Any {
public:
    Any(){}
    Any(const const_Any& c) : const_Any(c) {}
    // 非const メンバ関数
    // 必要なら const メンバ関数を非const でオーバロードする
};
Any::Any(const const_Any&) の代わりに const_Any::operator Any() でも よい。どちらがよいのだろう。よくわからない。どちらでもいいのかもしれない。

注意すべき所

  • メンバの増減はない。(sizeof(Any) == sizeof(const_Any))
  • Any::Any(const const_Any&) に explicit は付けてはいけない。
  • Any::Any(const const_Any&) を書いたため Any::Any() が必要になっている。
  • コメント中のオーバロードはオーバライドの誤りではない。 オーバロードすれば、Any から、const と 非const と両方呼べるのだ。
要点は、constクラスを基底にすることだ。VC++ の list::iterator は 逆になっている。(非const から const を派生している。Plaugerさんしっかりしてくれ)
(9/8/99 追記:VC++6.0 では、constから派生するように直っている。よしよし)

11/16の話も参照。


98/11/13(金)

fj.comp.lang.c++ で面白い話を聞いた。
    for (int i=0;i<n;++i) {
        ...
    }
    // ここで i は使える?
for の () の中で宣言した場合、スコープは、for の中だけでなければ いけない。しかし、VC++ を始め、そうなっていないコンパイラも多い。 それを回避するのが、以下のマクロだ。
    #define for if (false) ; else for
こうするとうまくいくのだ。なんでだろうか。 else は、実行されるかされないか判らないので、 for 以降も有効にならないようにしてある のだろう。 また、if の後に使ってもよいように工夫されている。 (つまり、if (true) for でもいいのだが、問題がある。)

但し、私ならこんなマクロを使うよりは、for の中で 宣言しないようにするだろう。


98/11/16(月)

11/12 の記事で勘違いしていることに気づいた。 わざわざ、「オーバロードはオーバライドの誤りではない」と書いていたのに、 実は誤りだった。恥ずかしい。 チェックすればいいものを、するまでもないと思ってしてなかった。

でも、何故、オーバロード(多重定義)にならないのだろう。不思議だ。

3/3の話も参照。


98/12/22(火)

1ヶ月ぶりの更新。この前、C++の本を買った。 本の名前は「プログラミング言語C++第3版」Bjarne Stroustrup 著、長尾高弘訳、7000円である。 実は、10年前に出た第1版は会社にもある。この第1版と第3版を見比べてみたが、 量的に3倍に増えている。内容も前より面白そうである。なかなかお勧めである。
この本を読むと、いろいろ知らないことにであった。例えば、
    if (int i = func()) {
        // ...
    }
こんな例が出ていた。これの利点は本に出ていた理由のほかに、 = が == の間違いでないとはっきりする所もあると思う。VC++5 でコンパイルできるが、 if 文の後で、変数 i が有効になっていないのが「なんだかなあ」である。 もちろん規格通りなのだろうが、for 文とあってない点が気持ち悪い。
この本の章の終わり毎に、練習問題があるのでやってみたい。できたら、 回答にまとめる。

目次 次へ