あどけない話

Internet technologies

どうしてこんなキーワードがあるの?

昨日、友達と呑んでいて、「C の switch、do-while、union を使ったことがない。どうしてこんなものがあるのか?」と聞かれました。その場で説明したんですが、「あどけない話にも書いて」と言われたので、書いておきます。(あまり、乗る気ではないのですが。。。)

switch

確かに if-else があれば不要です。でも、if-else の使い方の中で、頻繁に出てくるパターンがあり、それを取り出したのが switch です。すなわち、ある*一つの*スカラー変数の値に応じて、挙動を変えることです。

典型例は、コマンドオプションの処理でしょう。

while ((ch = getopt(argc, argv, "abc")) != -1) {
  switch (ch) {
  case 'a':
    /* -a の場合 */
    break;
  case 'b':
    /* -b の場合 */
    break;
  case '?':
    /* fall through */
  default:
    /* -? と知らないオプション */
    usage();
  }
}

上記のように、fall through をうまく使うと、処理をまとめられて便利です。

蛇足ですが、default: は下にある必要はありません。人生で一回だけ、上の方にあるコードを見たことがあります。まぁ、上の方に書くのはお勧めしませんけどね。

switch に関する議論はいくつかあります。一つは、デフォルトは break にすべきだという主張。「DのFAQ」には、こんなことが書かれています。

switch文がfall throughなのは何故ですか?

沢山の人に、switch 文のcaseとcaseの間には必ずbreakが入る、 という仕様にならないかと要求されました。C の、いわゆる "fall through" は沢山のバグの原因となってきたからです。

Dでこれを変えなかった理由は、整数の昇格や演算子の優先順位を同じにしたのと、 同様の理由 - Cとして読めるコードはCと同様に振る舞うべき - です。 潜在的に違う意味をもっていたりしたら、 余計わかりにくいバグを生むでしょうから。

デフォルトは break で、fall through のときに、新たなキーワード 'through' を使うというのはいいアイディアですね。

それから、「オブジェクト指向のこころ」の175ページには以下のようなことが書かれています。

switch は抽象化の必要を示す赤信号となり得る

switchは、(1)ボリモーフィズムの導入、あるいは、(2)責務の見直しを検討すべき赤信号となり得ます。この場合、抽象化を行ったり、他のオブジェクトに責務を委譲したりするといった、より一般的な解決策を考慮して下さい。

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)

C だと多相性(異なる種類のオブジェクトを同じ関数名で仕事をさせること)は実現できないと思っている人もいるかもしれませんが、関数へのポインタの配列を用意すれば、同じようなことはできますね。

do-while

これは簡単です。while は 0 回以上繰り返しなのに対し、do-while は 1 回以上の繰り返しなのです。

必ず1回は実行したいことの典型例は、入力です。

do {
  /* 入力 */
} while (/* 正しい入力でない間 */)

do-while が存在するのは、'*' で十分な正規表現に、'+" も定義されている理由と同じです。便利でスマートに見えるからです。

上記のコードを do-while を使わないと、こうなってしまいます。

/* 入力 */
while (/* 正しい入力でない間 */) {
  /* 入力 */
}

同じコードが 2 回現れるのが、汚いと感じる人もいるってことですね。

なお、for と while は等価なので、どちらか一方でいいはずです。do-while がいらないという人は、同時に for と while のどちらかはいらないと言わないとフェアではない気がします。。。

union

気持ちは分ります。関数へのポインタと union を使いこなせるようになれば、一人前の C プログラマーです。:) この二つが分らなければ、カーネルのコードは書けないでしょうね。

論より証拠ということで、ICMPv6 のヘッダの定義を見てみましょう。(質問した友達は、ネットワークのことが分るので、例はこれで十分でしょう。)

struct icmp6_hdr {
        u_int8_t        icmp6_type;     /* type field */
        u_int8_t        icmp6_code;     /* code field */
        u_int16_t       icmp6_cksum;    /* checksum field */
        union {
                u_int32_t       icmp6_un_data32[1]; /* type-specific field */
                u_int16_t       icmp6_un_data16[2]; /* type-specific field */
                u_int8_t        icmp6_un_data8[4];  /* type-specific field */
        } icmp6_dataun;
};

#define icmp6_data32    icmp6_dataun.icmp6_un_data32
#define icmp6_data16    icmp6_dataun.icmp6_un_data16
#define icmp6_data8     icmp6_dataun.icmp6_un_data8
#define icmp6_pptr      icmp6_data32[0]         /* parameter prob */
#define icmp6_mtu       icmp6_data32[0]         /* packet too big */
#define icmp6_id        icmp6_data16[0]         /* echo request/reply */
#define icmp6_seq       icmp6_data16[1]         /* echo request/reply */
#define icmp6_maxdelay  icmp6_data16[0]         /* mcast group membership */

つまり、異なるデータ型がメモリの同じ位置に来る場合は、union を使うべきなのです。

へなちょこプログラマーは、union を使わずに、キャストを利用してバグの多いコードを書きがちです。

ちゃぶ台をひっくり返すようなまとめ

なぜ Haskell は重要か」の一部を翻訳して、この記事のまとめとします。

(関数型言語と)同じ程度とは言えないが、命令を並べる方式でも抽象化していくとこはできる。命令型言語では、新しいキーワードと標準ライブラリを導入することで抽象化する。たとえば、多くの命令型言語は、プログラマーがループを実現する仕事から解放されるように、少しずつ動作が異なる複数のキーワードを用意している。しかし、命令型言語は、命令を並べる方式に基づいているため、そこから完全に逃れることはできない。命令型言語では、命令の並びに対し、抽象化のレベルを上げる唯一の方法は、さらなるキーワードや標準関数の導入である。そして、言語はゴチャゴチャになる。

来れ!純粋関数型言語の世界へ。(ああ、無理矢理だ。:)