Programming Tips 1024

プログラムしているときに思いついた小ネタを書きます。このコーナーへの投稿もお待ちしてます^^
特にことわりが無い限り、C++/VC++/Win32です。情報提供者orネタ提供者がいる場合は、表題の右横に《 》で書いています。


'01/10/08

Tips 51.  functorを活用する

functorについては、rsp(スーパープログラマへの道)の、

>第C7回  functorは函数合成の夢を見るか(何のこっちゃ^^;) 00/09/27
>第D5回  倒れるまでヤキを食え!(いいかげんプログラムってうっとおしいねん) 01/01/17

で、少し書いていますが、インライン展開されるところがミソです。yaneSDK2ndのCDIB32Effect::Blt関数等が、これを使っていたりします。

そのメインの転送部分は、これです。

for(int x=rcClipRect.left; x < rcClipRect.right; x++){
 // 転送元から転送先へ、この関数エフェクトをかけながら転送
 f(*lpDst,*lpSrc); // functor is greatest..
 lpDst++;lpSrc++;
}

非常に遅いコードであるのは、Tip.48〜Tip.50を読まれた方には、わかるでしょう。これでは何のために、functorを使用しているのか、わかったもんではありません。functorの最大のメリットは、インライン展開される、という部分です。よって、上のコードは、以下のように修正すべきです。

// 8ループ展開
int nLoop8 = (r.right - r.left);
// 8ループ余り
int nLoop8mod = nLoop8 & 7;
nLoop8 >>= 3;

       (中略)

for(int n=0;n<nLoop8;n++){
// 転送元から転送先へ、この関数エフェクトをかけながら転送
 f(lpDst[0],lpSrc[0]);
 f(lpDst[1],lpSrc[1]);
 f(lpDst[2],lpSrc[2]);
 f(lpDst[3],lpSrc[3]);
 f(lpDst[4],lpSrc[4]);
 f(lpDst[5],lpSrc[5]);
 f(lpDst[6],lpSrc[6]);
 f(lpDst[7],lpSrc[7]);
 lpDst+=8; lpSrc+=8;
}
// 余剰分を、転送
for (n=0;n<nLoop8mod;n++){
 f(lpDst[n],lpSrc[n]);
}

しかし、このfunctor fがある程度複雑で、lpDst,lpSrcのレジスタ割付が阻害されるようになってくると、このようにインデックスでアクセスするほうが速いのかは微妙になってきます。(ループを展開している部分は確実に早いのですが)


Tips 52.  \でなく / を使えと言ったのは誰だ

C言語では、'\'は、'\\'のように書かなければなりません。日本では円マークになっていますが、海外ではバックスラッシュになっています。ASCIIコードで言えば、0x5c。どうして、こんな文字をエスケープとして使ったのかは、やねうらおの知るところではありませんが、非常にうっとおしい文字であることには違いないでしょう。

Windowsでは、'/'(スラッシュ)でもフォルダ指定できるので、こちらを使いなさい、という風に言われるのも無理ないところです。(私もそう思っていました) ところが、DirectShowでwmvファイルが再生できないということで、カイネさんに調べてもらっていたんですが、わかったことは、

ということなので、wmvのDirectShowCodecは'/'区切りのファイル名は認識でき
ない、という問題があるのではないかと考えられます。

むむむむむ、、。きっと、このCodec、かなり古いAPIを呼び出してファイルを読み込んでいるのか内部で独自に読み込んでいるのか、そのへんが理由であることは容易に推測できますが、DirectShowのCodecって言ったら、作られたの最近でないですか。なんでそんなことするかなー..とか思うわけであります。

というわけで、yaneSDK2ndでは、ファイル読み込みクラスでフルパス名に変換するときに、'/'は'\'に置換するという処理を入れることにしました。非常に泣かせる結果です。


'01/10/15

Tips 53.  ド・モルガンの則

2項に対するド・モルガン則は、高校でも習います。あまりにも有名ですね。

not(A ∪ B) = notA ∩ notB
not(A ∩ B) = notA ∪ notB

C言語風の表記で書けば、上の2つは、

!(A || B) == !A && !B
!(A && B) == !A || !B

です。

なぜこんなことを問題とするのかと言うと、whileやforには、ループの終了判定のための条件式を書かなくてはならないのですが、こいつが、人間の思考に反しているというか、ループは繰り返しなのですから、「ループを終了するための条件」を書かせて欲しいのです。

どうして、誰もこのことを問題としないのか、私には不思議でなりません。あまりにも初歩的な事項すぎて、忘れられていたり、あまり議論にのぼったりしないので、この場を借りていくつか書きたいと思います。

つまり、終了するための条件を頭のなかで考えてしまう普通の(?)思考法である私は、こいつの否定条件形を書かなければならないのです。たとえば、A || Bのときに抜けるならば、その否定 !(A || B) == !A && !Bをwhileの条件式に書かなければなりません。!(A || B)と書いてもいいんですけど、あまり格好良くないので、!A && !Bの形で書くのが普通です。

だから、否定形を作り出すための法則、すなわち、ド・モルガン則が必要になってくるのです。

例:
xが100以上か、yが100以上になればループを終了
⇒ 「x>=100」か「y>=100」でループ終了
⇒ ループを抜けない条件は、『「x>=100」か「y>=100」』で無いこと
⇒ !((x>=100)||(y>=100))
《ド・モルガン則の適用 !(x>=100) && !(y>=100)
⇒ (x<100) && (y<100)

というようになります。


Tips 54.  多項ド・モルガン則

では、3項の場合について考えてみます。結論から言えば、

not(A ∪ B ∪ B) = notA ∩ notB ∩ notC
not(A ∩ B ∩ C) = notA ∪ notB ∪ notC

です。多項になっても、ド・モルガン則は成立します。

これを証明するのは面倒なので、duality(双対性)という概念を持ち出します。

つまり、論理集合の定義

U={T,F}       // 全集合はT(true) or F(false)から成る
not T = F,not F=T  // trueの否定はfalse,falseの否定はtrue

また、∪(or)と∩(and)の定義は、

A∪B A∩B
A=F,B=F
A=F,B=T
A=T,B=F
A=T,B=T

ですから、∪と∩,FとTが対称(双対)になっています。(U⇔∩とF⇔Tを同時に入れ替えても成り立ちます)

定義において、このような双対性があるので、ド・モルガン則が成立するのは、これより明らかです。(FとTを入れ替えるというのは、notを付けると解釈してください)

※ 大学院で使うような論理数学の教科書には、この双対性というのがちらっと出てきますが、こんなものは、高校で教えるべきだと思います。ド・モルガン則を証明するより、よっぽど簡単です。


Tips 55.  論理の分配則

また、分配則も成り立ちます。

(A ∪ B) ∩ C = (A ∩ C) ∪ (B ∩ C)
(A ∩ B) ∪ C = (A ∪ C) ∩ (B ∪ C)

上のと下のとは、∪と∩が入れ替わっただけ(双対)というのに着目すれば、覚えるのは、どちらか一つで良いです。私は上式を覚えています。上式は、∪を+,∩を×と考えれば、

(A + B) × C = (A × C) + (B × C)

という、普通の分配則と同じですから、直感的に理解できるでしょう。


Tips 56.  elseだけ書いてはいけないのか?

Tips.53〜Tips.55で、論理数学の簡単なおさらい(?)をしました。プログラミングしていて、時々使うので覚えておいて損は無いです。

ただ、私が言いたいのは、そんなことでは無いのです。これを書くきっかけとなったのは、以下のプログラムです。

if ( b1>=10) {  
} else {
 init( );
}

省略しているのではありません。正真正銘、if成立時の式はありません。とんだド素人のプログラムだと、お笑いになるかも知れませんが、これ、私が最近書いたプログラムの一部です。どうして、こう

if ( b1<10) {  
 init( );
}

書かないのか、と問われるかも知れません。そう書かない理由は、他の場所から、コピペしてきたからです。そのコピペする元では、ifの成立時の処理が書かれていました。しかし、コピーしてきた、こちらには、その処理は必要なかったので、ブランクにしました。

しかし、これを保守していた、プログラマにとって、この部分がとても気持ち悪かったらしく、勝手に後者の形式に書き換えてしまいました。ですが、これは明らかな改悪なのです。これを書き換えたプログラマは、単に自分の感覚になじまないという理由だけで書き換えてしまいました。なぜ、これがいけないか、彼は理解できていませんし、おそらく、これを読んでおられる方のほとんどの方も、同様でしょう。

もう少し詳しく説明します。前者と後者は、if式のなかが、否定形になっています。それが最大の問題なのです。コピペしてきているわけで、何かの要請によって、コピペ元に条件を追加しなくてはならなくなるかも知れません。たとえば、条件式がこのように

if ( b1>=10 || b2>=10) {  

修正されたときのことを考えてみましょう。先の部分は、ここからコピペしているわけで、当然、この修正を反映しなくてはなりません。前者の形式ならば、このままコピペすれば、それで作業は完了です。ところが、後者のように書き換えていたとしたら、どうすれば良いでしょうか?

ここまで読んで来られた方ならば、Tips.53のド・モルガン則を適用して、

if ( b1<10 && b2<10) {  

と書き換えなくてはならないことはおわかりでしょう。ある程度の熟練されたプログラマならば、これは無意識に出来ます。この程度が無意識に出来ないのならば、プログラマ失格と言っても過言では無いです。しかしですね!コピペで済むものを、わざわざこんな書き換えはしたく無いのですよ!!

さらにコピペ元に条件が追加されたときのことを考えてみましょう。

if ((b1>=10 || b2>=10) && (b3>=10)) {  

これ、さきほどの後者の形式だと、コピペ先は、Tips.54の多項ド・モルガン則を適用して、

if (( b1<10 && b2<10) || (b3<10)) {  

と書き換えなくてはなりません。理屈ではそうですが、こんな保守、やらされるほうにすればたまったものではありません。そもそも、これだけ形が違うと、コピペしてきてあることも気付きにくいですし、この部分が前者の論理の否定形であることすら気付かないです。よしんば気付いたとしても、誰でもこの書き換えをさらっと出来るわけでも無いでしょう。

もう、私の言いたいことが、わかっていただけたでしょうか。後者の形式は、何もすっきりなどしていないのです。「前者の形式が気持ち悪いから後者の形式に書き換えました」なんて言うのは、そのプログラマの自己満足に過ぎません。

これの応用で、私は条件式を否定形で書きたく無いがゆえに、わざと if 成立時の処理はブランクにして、elseで書くことがあります。これを、とんだド素人のプログラムだと考るのか、熟練されたプログラマのプログラムと考えるのかは、読者のご判断にお任せしますが..。


Tips 57.  安全性チェックは境界条件でやる

ある文字列を表示するプログラム(関数)を書いたとしましょう。

少なくとも、テストしなければいけないのは境界値です。すなわち、最小値である0(ゼロ)バイトのとき、どうなるのか。そして、3ではどうか。3で動けば任意の数nでも動くと考えられます。あと忘れてはいけないのが、∞(ある程度大きい値)で、バッファオーバーに対する安全性について調べるのです。

このように、極端な部分で必ずテストする習慣が必要です。特に、ゼロと∞は忘れがちなので、必ずやりましょう。

たとえば、wsprintfでは、1,024バイトまでのバッファしか扱えません。それ以上の場合は、_snwprintf 関数を使います。∞でテストしないと、いつまでたっても、こういう制限があることに気付かないのです。


'01/10/28

Tips 58.  成立時脱出ループ 《れむ》

Tips.53で、whileの条件式として、ループを抜ける条件を書かせて欲しいという話をしました。もちろん、書けます。

while(true) {
 if (終了条件) break;
 // 以下、ループ本体
}

実際のところ、こうやって書いたほうが、楽な部分も結構あります。永久ループのための構文が一つあって、continueとbreakとgotoさえ実装されていれば、それで良いという人も結構います。私も、そうだと思います。while (true)の部分は、人によって好みがあって、while ( 1 )やfor( ; ; ) (←泣いてる?^^;)等も考えられるところです。

たいてい、こういうプログラムは、リファクタリング(改良)の対象となるかも知れません。要するに、もっと洗練された、美しいコードにせえ!と誰かに突っ込まれることでしょう。しかし、果たして本当にそんな必要があるでしょうか? 上記コードを、仮に

while( ! 終了条件) {
 // 以下、ループ本体
}

と書き直したところで、実際のところ、そこからは何も生まれません。そればかりか、前者は、if式の寸前に、何か処理を書ける余地があるのに、後者にはその柔軟性がありません。つまり、後者の形は、前者の縮退した、特殊な例であり、ループとして、柔軟性、一般性があるのは前者なのです。(だから、前者のスタイルが優れているという結論にもなりませんが)

私は良く思うのですが、使っていないものを削除したりすることが改善では無いのです。(見かけはスッキリしますが) また、プログラムは、同じ内容のものを少ないコード量で書く競技でも無いのです。それさえ理解していれば、前者のスタイルが生理的に許せない、ということも無いと思うのですが..。


Tips 59.  条件式はbooleanでは無いのか?

やねうらおのように、Cをやる前にPASCAL系の言語をやってましたというお行儀のいい坊ちゃん育ちなプログラマ(んなわけないか..)は、if 式の中に、ぽんとint型変数が書いてあるのが許せません。

if ( !x ) {

みたいな奴ね。これ、なんやねん?とちょっと思ったりもします。

if ( x==0 ) {

でええやん?と思います。C++は強く型付けされて(strongly typed)いるわけですから、ifのなかは、論理値すなわち、boolean(ブール代数型)でなくては!と思います。(C#ではそうなっています) まあ、Cにはbool型自体存在しなかったので、このへんは仕方ないのかも知れません。

同様に、Tips 57の、while ( 1 )も、それはどうなんや?と思ったりもします。まあ、そのへんは好みなので、どうでもいいと言えばそうなんですが、何か、int型もbool型も違いがわかっていなさそうで、いかにも頭悪そうです。^^;

ところで、次のプログラムは合法(legal)なのでしょうか?

if ( b == true) {

bはbool型の変数です。ですから、わざわざb==trueとしている意図が見えません。よく、こう書いている人が居るんですが、こんなものは削除すべきだと思うのです。

しかし、これが合法かどうかというと、少しややこしいです。The Annotated C++ Reference Manualによると、

C19-§2.5.5 ブーリアン・リテラルとして、型boolには、trueとfalseという2つのリテラルが有る。

私、この日本語、とてもまずい表現だと思うのですが、「2つ有る」と言う場合、それ以外には有るのか無いのかわかりません。常識的に考えれば他には無いのでしょうけれど、有ったところで論理的には矛盾しません。まあ、b == trueと書いてもプログラムは正しく動くんですけど(VC++で確認)、この本には、次のような記述もあります。

C19-§3.6.1 boolは独立した符号付きの整数の型である。boolの値は昇格によってintに変換されても良い。
この際、tureは1に変わり、falseは0に変わる。数値またはポインタ値は暗黙のうちにbool型に変換されてよい。
この際、0はfalseに変わり、非0はtrueに変わる。関係演算子はbool値を返す。

それで、結局、true/falseの定義は見つから無いのです。まあ、bool型の(のみの)リテラルとして考えて良いのでしょうが。これ↑を見る限りは、仮にtrueが1,falseが0だとしても、暗黙に変換されるわけで、b == trueの部分は、うまく動くことは保証されそうではあります。

私はC++ではなくCだったころ、コンパイラによってはboolはintをtypedefしてあるかも知れない、とよく言われたことがあります。しかし、いまはbool型がpremitiveな型としてサポートされているので、C++の開発では、そんな心配は無いようにも思います。しかし、Win32の開発でときどき出てくるBOOL型とは何なのでしょうか?実際、MapInles.hで大文字のBOOLはintとtypedefされています。ということは、BOOLは、0か1以外の値も取り得るわけで、BOOLを使っているときに、

if ( b == true) {

とか書いていると、このBOOL値が0か1しか取っていないとは限りませんから、そのうち痛い目に遭いそうな気もします。


Tips 60.  エラー処理の記述の仕方 《Toru》

普通、エラー処理は、Aという処理をやって、失敗しているかをifで判定して、失敗していればエラーリターン、Bという処理をやって、失敗していればエラーリターン、、という方式で行なうものと思います。ところが、こうやると、Bという処理をやって失敗した場合、Aという処理の終了処理を呼び出す必要が往々にして有ったりします。

たとえば、ファイルを2つオープンして、コピーするようなプログラムでもそうです。二つ目のファイルオープンに失敗すれば、一つ目のファイルを閉じなければなりません。ファイルのオープン〜クローズが、何らかのクラス化されていれば、スコープアウトされるときに、デストラクタが自動的に解決してくれるかも知れませんが、そんなケースばかりとも限りません。

このときに、次のようなスタイルで書けばどうか、というのがToruさんの提案です。

 HANDLE hFile = CreateFile(...);
  if (hFile != INVALID_HANDLE_VALUE)
  {
   if (GetFileSize(...) != INVALID_FILE_SIZE)
   {
     ....
   }
   ....
   CloseHandle(...); ←ここ一回で済みます
 }

なるほど。これは、正しいフローですし、美しいし、いいことずくめのように思えます。ところが、私は「このスタイルだと、if文が10個あると、10個インデントされて、現実的なプログラミングスタイルにならないのでは?」とToruさんに尋ねたところ、「やっぱり例外を使うべきですかねぇ..」と言われました。

うーむ。私の尋ね方が少し意地悪だったかも知れません。10個インデントされて困るならば、ifでインデントするのをやめれば良いだとか、エラーリターンすべきところをgotoで書いてしまえば良いだとか、そういうレベルでも良いと思うんです。

 HANDLE hFile = CreateFile(...);
 if (hFile == INVALID_HANDLE_VALUE)
  { lResult = 1; goto CLOSE; }

 if (GetFileSize(...) == INVALID_FILE_SIZE)
 { lResult = 2; goto CLOSE; }

     ....
CLOSE:;
 CloseHandle(...);
 return lResult;

前者が見やすいか、後者が見やすいか、それとも、gotoで飛ばさず、直接CloseHandle(..); return 1; というように終了処理を埋め込むほうが良いのか、ファイルのオープン〜クローズをinner class化するのが良いのか、それは、終了処理の量、全体としてのコード長さ、最大インデントの数によりけりです。いつでもこのスタイルで良い、ということは無いと思うのです。


Tips 61.  &&の評価順

ビット演算& の評価は、右からか左からか定義されていません。同様に、

a = func( ) + func( ) * 2;

このように書いた場合、どちらのfuncが先に呼び出されるかも定義されていません。VC++では、これについて面白い現象があるので、さ〜さんのページを参考として挙げておきます。

http://www.aya.or.jp/%7Esanami/peace/memorial/code31-40.html#CODE36

しかし、&&は違います。必ず左から右です。

Perlでは&&や || は、ショートカットオペレータと呼ばれていて、&&の左の式が偽ならば、&&の右の式は評価(実行)されないのです。そこで、ファイルのオープンに失敗したときにエラー処理をするプログラムは、

 !OpenFile( ) && ErrorExit( );

と書けます。同様に、C/C++でも、これは書けます。よくあるのが、

if ( p && p->func( )) {

というものです。pが非NULL(C++ではNULL==0が保証されているので、こう書けます)であるときだけ、ある関数を呼び出すというものです。第一のオペランドが真のときだけ、第二オペランドを評価することは保証されています。(同様に、 || は、第一オペランドが偽のときのみ第二オペランドを評価することが保証されています)

これを使わないと、

if ( p && p->func( )) { A; } else { B; }

のように書いてあるものは、

if ( p ) {
 if ( p->func( )) { A; } else { B; }
} else { B; }

とBを2回書くか、

bool b=false;
if (p) { b = p->func( ); }
if (b) { A;} else {B;}

とするか、何にせよ、面倒くさそうです。


Tips 62.  &と>>の組み合わせ

&でマスクをとったあと、シフトしたい、ということが往々にしてあります。たとえば、

r = (rgb & 0xff0000) >> 16;
g = (rgb & 0xff00) >> 8;
b = (rgb & 0xff);

です。しかし、これは、

r = (rgb >> 16) & 0xff;
g = (rgb >> 8 ) & 0xff;
b = (rgb     ) & 0xff;

と、シフトを先に行なえば、ビットマスクは1バイトで済みますんで、x86系のCPUでは、後者のほうが短いコードになります。案外盲点になっているので、気をつけてみてはどうでしょうか。どちらが見やすいコードかというと、また話は別ですが。

バリエーションとして、 (r << 8) & 0xff00 は、 (r & 0xff) << 8; と、先にマスクをとります。


'01/10/29

Tips 63.  2進数で小数を表現する

2進数の浮動小数点表記としてIEEEの、、なんて話はしません(笑)

スケーリングの話です。仮にいま、256倍にスケーリングして、1.0を256として表現するときのことを考えてみましょう。

このとき、0.5は128として表現されます。0.25は、64として表現されます。0.75ならば、64+128=192というようになります。
このように、小数部は、8ビット(のみ)で表されます。同様に65536倍(2^16)にスケーリングすれば、小数部は16ビット(のみ)で表されます。

小数部を8ビットとして、割り算をする例:

div_xy8 = (x << 8) / y;

このように、整数のみを使っていながら、それを仮想的に小数とみなすことが出来ます。これが、スケーリングの大きな特徴です。


Tips 64.  sinをテーブル化する

Tips.63で、小数を整数で表現する枠組みが出来ましたので、さっそくsinのテーブルを考えてみましょう。sinは最大で1.0です。よって、1.0を256と考えれば、1.0以外は1バイトで表現できます。まあ、テーブルサイズをBYTEにする必要も無いので、WORDで良いんですが。角度も、1周360゜(degrees)や、1周が2π(radian)というのも、うっとおしいので、これもスケーリングして、一周を256だとか65536のほうが話が早いです。

sinをテーブル化してしまえば、cosもそのテーブルを利用できます。仮に1周を256でスケーリングしてあるsinテーブルを利用するならば、

cos_x = sin_table[(x + 64)&0xff];

です。


Tips 65.  atanをテーブル化する

tanの逆関数、atanというのがあります。角度を求めるものです。高校の数学では普通、y/xの形で与えますが、これだとx == 0のときに、この割り算結果が∞になってしまうので、Cの関数としてはatanではなくatan2という、(x,y)の形を与えて、その角度を返す関数を用います。

この関数、何に使うかと言いますと、シューティングゲーム等で、自機をめがけて発射される弾の方向を算出するのに必須なのです。

atan自体の(まともな)計算方法は、奥村晴彦氏の「C言語によるアルゴリズム事典」等に、テーラー展開あるいは連分数展開で求める方法が紹介されていたと思いますが、そんなものでは役に立たないのです。

また、単純に考えると、y/xのテーブルというのは、非常に大きそうにも思えます。目的の値を探すのが、非常に時間がかかりそうに思われます。一見すると、atanは単調増加だから、2分サーチが使えるだとか、そういう話にもなりそうです。

まったく違うのです。atanは、そんなことをしなくてもテーブルの一発参照で済む方法があります。

それは場合分けです。x ≦ y と x > yの場合を分けてしまうことです。こうすれば、たとえば前者、x ≦ yの場合ならば、(x / y) ≦ 1が保証されます。

演算結果が1以下であることが保証されるということは、(1を無視すれば)小数部しか無いということです。Tips.63の方法を用いれば、その結果は(どんな値であれ)8ビットやら16ビットやらで表現できるということです。ということは、テーブル参照できるということです。

atan_table [ (x << 8) / y ];

簡単ですね。以下、x > yの場合も、atanの特性から、このテーブルを利用できます。

実装としては、このxとyの大小関係による場合分け( x ≦ y と x > y)と、象限ごとの場合分け(第1象限x >= 0 , y >= 0 , 第2象限x < 0 , y >= 0 ,第3象限x < 0 , y < 0 ,第4象限x >=  0 , y < 0)の2×4で8つの場合分けをして、それぞれ上記のatan_tableを使って求めればokです。


'02/05/12

Tips 66.  ディフォルト引数

どうでも良いことですが、引数は、「ひきすう」と読みます。「いんすう」と読むと相手は「因数」だと勘違いします。^^;

よく初心者にありがちなのが、こういうチューンです。

  f(int x,int y);

この関数に、第3パラメータを用意して..

  f(int x,int y,bool b=true); 

こうしておけば、いままでとは関数インターフェースが変化しないので、従来のプログラムも正しく動くと考えますね。ところが、世の中、そう甘くなかったり、甘かったりするのです。(どないやねん!)

上のように変えたとき、コンパイルが通らなくなるケースは、ずばり、この関数ポインタを用いて何かをやっていたときです。関数プロトタイプが変化するので、この関数を関数ポインタ経由で呼び出していたりするプログラムはコンパイルが通らなくなるのです。

ちゅーことで、気をつけてください(誰にゆうてんの..>私)


Tips 67.  続コメント算法 《ちょぴ

/* 〜 */のコメント文について、こんな意見をいただいたので紹介させていただきます。

Tips45のお手軽コメントの応用です。

//*/
  リリースコード();
/*/
  デバッグコード();
//*/

この状態だと、リリースコードだけが動きます。ここから、
先頭のスラッシュを取ると…。

/*/
  リリースコード();
/*/
  デバッグコード();
//*/

デバッグコードだけが動きます。ここから真中の後ろスラッシュを取ると…。

/*/
  リリースコード();
/*
  デバッグコード();
//*/

リリースコードもデバッグコードもコメントアウトです。
で、最初の状態から、真中にスラッシュを入れると…。

//*/
  リリースコード();
//*/
  デバッグコード();
//*/

リリースコードもデバッグコードも動きます。
リリース・デバッグの有り無し4パターンすべてを、最大でも3個の
スラッシュ付け外しだけで制御できます。

で、コードをもっと増やしていった場合の一般形はたぶんこう。

//*/
  コードA();
/*/
  コードB();
/*/
  コードC();
//*/

スラッシュをつけたりはずしたりすると、いろいろコントロールできて
楽しいです。

/*〜*/ のコメントはネストの問題がありますけど、「注釈」としての
コメントはすべて // で済ませるように習慣づけてれば、
結構使えるのではないかと。
#if 0 〜 #else 〜 #endif よりも柔軟性が高いし、VCなら
コメントアウト部分は色を変えてくれるからこっちのほうが
見やすいと思います。


適当ですけど、「究極のコメント」と称して、上のコメントテクニックを
まとめてみました。

[コメント記述子]
 [コード]
 [コード]
[コメント記述子]
 [コード]
 [コード]
[コメント記述子]

このような形で記述するのが基本形です。
正規表現ちっくに書くと、

((コメント記述子)(コード)*)*(コメント記述子)

だと思います(自信なし)。
コメント記述子は、次にコメント記述子が現れるまでの複数のコード
(以下、コードブロック)がコメントであるか否かを制御できます。

コメント記述子は、以下の3つがあります(ほかにもあるかも)。

/*
『スタート』
次にコメント記述子が現れるまでのコードブロックを、強制的に
コメントにします。

/*/
『リバース』
このコメント記述子が現れるまでのコードブロックがコメントなら
それ以降(次にコメント記述子が現れるまで)は有効なコードに。
逆にそれまでが有効なコードだったなら、それ以降(次にコメント
記述子が現れるまで)はコメントになります。

//*/
『エンド』
次にコメント記述子が現れるまでのコードブロックを、強制的に
有効なコードにします。

『エンド』は単独でソース内に記述しても「コードブロックを
分割する」以上の意味を持たないので、任意に挿入できます。
『エンド』からスラッシュをひとつ削ると『リバース』に。
『リバース』からスラッシュをひとつ削ると『スタート』に。
つまり、スラッシュを削ればコメントが増えていき(単調増加?)、
スラッシュを付けていけばコメントが減っていきます。

また、『エンド』はコメント記述子の「終端」も意味します。
コメント記述子は任意の順番で書けますが、最後に現れる
コメント記述子だけは、必ず『エンド』にしてください。

以上、「究極のコメント」の簡単な説明でした。この説明をふまえた上で
上のテクニックを見ると、しくみが容易に理解できます。

(´-`).。oO(誰か「至高のコメント」を…!)

Tips 68.  vector<bool>

vectorのなかで、vector<bool>だけは特殊な存在です。メイヤーさん(Scotte Meyers)は、その著書『EffectiveSTL』のなかで、「vector<bool>は使わないようにしよう」という項タイトルで、

1.STLのコンテナでないこと
2.bool型の値が格納されないこと

をその欠点としてあげておられます。すなわち、

vector <bool> v;
bool *pb = &v[0];

このコードはコンパイルされないのです。..って、そうなのかよ!いま知ったよ(笑)

これは、vector<bool>は、擬似コンテナで、実際のbool型の値を格納しているわけではないからです。1バイトには8個のbool型の値が格納されているのです。これは、プロキシオブジェクトを作成して、operator [ ]によって読み出しを書き込みから区別する(⇒『More Effective C++』第30項 proxy class)技法で実装されています。

別にこの実装で困ることはそんなに無いのですが、上のコードがコンパイルできないと困ることもあるかも知れません。そこで、もし、上のコードがコンパイルしたいのなら、vector<BOOL>を用いて、使うときにbool*にキャストするというのはどうでしょうか。

vector <BOOL> v;
bool *pb = reinterpret_cast<bool*>(&v[0]);

と、ここまで書いたのちにVC++6.0のSTLで、

vector <bool> v;
bool *pb = &v[0];

をコンパイルすると、正しく動くんですよ。ん??動くの?^^; 気になったので、vectorのヘッダを見る。

// CLASS vector<_Bool, allocator>
typedef unsigned int _Vbase;
const int _VBITS = CHAR_BIT * sizeof (_Vbase);
typedef allocator<_Vbase> _Bool_allocator;
class vector<_Bool, _Bool_allocator> {

確かに、その定義っぽいんですけど、ディフォルトallocatorの指定が無いです。ということは、VC++6では、vector<bool>と書くだけでは、このクラスが実体化されるわけではないようです。


Tips 69.  多次元配列

固定長配列を使うのはやめて、vectorにしなさいというのは、もはや常識(?)だと思うのですが、vectorによる多次元配列というのはどのようになるのでしょうか?

配列の配列を作れば良いので、以下のようになります。

typedef vector<bool> vect_bool;
vector<vect_bool> v(x);

vectorのコンストラクタの第1パラメータは、配列サイズ、第2パラメータは初期化のためのパラメータを指定できます。

たとえば、

vector<int> v(100,200);

これならば、200という数字で初期化された、100個の要素を持つint配列が出来ます。上の2つの技法を組み合わせれば、

typedef vector<bool> vect_bool;
vector<vect_bool> v(x , vect_bool(y));

x×yなる2次元配列を用意できます。trueで初期化された、2次元配列が欲しければ、

typedef vector<bool> vect_bool;
vector<vect_bool> v(x , vect_bool(y , true));

で良いですね。このあと、

v[3][2] = true;

というように、2次元配列らしくアクセスできます。


Tips 70.  オートコンプリート

Visual Studioのオートコンプリートは、「.」や「->」を打ち込んだときにだけ働くと思っている人が多いですが、入力中にCTRL + SPACEで強制的にオートコンプリートをさせることができます。

※ メンバのオートコンプリートについてはTips.41にあるように、this->をダミーでタイプするのですが、これはVSのマクロを用います。

Sub this_macro()
ActiveDocument.Selection = "this->"
ExecuteCommand "CompleteWord" ' ※
End Sub

※ しかし、CompleteWordは、マクロ中の記述は無効のようなので、実際はこれではオートコンプリートされないようです。仕方ないので、このマクロをshift+ctrl+spaceに割り当てて、shift+ctrl+space ⇒ ctrl + spaceと連続して押すことによって、メンバのオートコンプリートを実現するといいと思います。


戻る