第三回 GNU C の書き方 (3)

GCC 特有の機能を使う

今回は GCC でのみ有効な機能について説明しよう。 詳細は GCC のマニュアル を参照して欲しい。 しかし、当然のことながら他のコンパイラでは全然駄目になる可能性が高いので、 GCC だけで本当に良い場合に限って使うべきだし、通常はなるべく使わない方が良いだろう。

移植性を損なわずにそうした拡張機能を利用するには、 __GNUC__ というマクロの有無をチェックすれば良い。 しかし、__GNUC__ が定義されていないときもちゃんと記述できなければならないので、これは必ずしも容易ではない。 ちゃんと他では駄目かもしれないということを念頭に置いて、十分注意して使って欲しい。

最大値を返す関数

ここでは、最大値を返す関数(またはマクロ)、max を例として採用することにする。 これは非常に多くの場面で使いたくなる機能であるし、なかなか興味深い関数だからだ。

まず、もっとも可搬性に富み、もっとも問題の生じにくいやり方を示す。

   1 int
   2 max (int a, int b)
   3 {
   4   return (a > b) ? a : b;
   5 }

普通に関数として定義するやり方である。 ここにはいくつかの欠点があるが、一つは速度上の問題である。 たかがこれだけの処理に、関数呼び出しのオーバヘッドをかけるのはもったいない。 そこで、関数呼び出しを行うことなく定義するのに、インライン関数を使ってみよう。 インライン関数は名前の通り、中身が展開される。

   1 inline int
   2 max (int a, int b)
   3 {
   4   return (a > b) ? a : b;
   5 }

inline を頭に付けるだけである。 これによって、関数の中身が呼び出されるところで展開される。 こうすると、あたかも通常の関数呼び出しのように書きつつも、実際には呼び出さないことが可能となる。 インライン関数を展開するには、コマンドラインのオプションに、最適化のオプションである -O を付けなければならないことに注意しよう。 そうでないと、GCC は単に inline を無視して、通常の関数として扱ってしまう。

マクロを使って

しかしここにはまだ問題がある。 max 関数は型が int しか扱えないのである。 もちろん、より小さな型である short intunsigned char なども扱えるが、そこには型変換のコストがかかってしまうことになる。 より重要なのは、浮動小数点を扱えないことだ。

もしこのままの方式でやると、それぞれの型毎に関数を作らないといけないことになる。 例えば、max_intmax_float のように。 しかしこれは明らかに格好良くない。 そこでマクロを使うことにしよう。 まず、もっとも素直に見えるやり方を示す。

   1 #define max(a, b)       ((a) > (b)) ? (a) : (b)

この欠点は副作用である。 非常に有名なので、すでに知っていると思うが、もし次のように呼び出されると、直感的でない結果が生じる。

   1   int i = 0;
   2   int m;
   3 
   4   m = max (0, ++i);
   5   printf ("%d, %d\n", i, m);

普通この結果は

1, 1

であることが期待される。 しかし実際には

2, 2

となる。なぜなら、この max の呼び出しは次のように展開されるからだ。

   1   m = ((0) > (++i)) ? (0) : (++i);

++i が二回評価されるわけである。 このような副作用はいろんなやり方で生じるので、防ぐ手段が必要である。

そこで GCC の compound statement の機能を利用することにする。 これを使うと、大括弧で括った任意の式を記述することができる。 すると、こう書ける。

   1 #define max(a, b)       ({ int _a = (a), _b = (b); _a > _b ? _a : _b; })

compound statement では最後に記述した式の値が全体の値として返ることになっている。 だからこの場合は、_a、または、_b のどちらか大きい方が返ることになる。 また、マクロを一度だけしか展開しないことによって副作用を避けている。 さっきの呼び出し例をこの方法で展開すると次のようになる。

   1   m = ({ int _a = (0), _b = (++i); _a > _b ? _a : _b; })

見事に副作用が避けられ、期待した結果が生じることが分かるだろう。

しかしこれでは、本来の目的であった型独立が破られてしまった。 なぜなら、compound statement の中では変数を宣言せねばならず、普通の C では、あらかじめ型を静的に指定しなければならないからである。 そこで、「式の型に名前を付ける」という拡張を利用してみよう。このためには typedef を普通ではない方法で使用する。

   1 #define max(a, b)       \
   2   ({ typedef _ta = (a), _tb = (b);      \
   3      _ta _a = (a); _tb _b = (b);        \
   4      _a > _b ? _a : _b; })

説明しよう。 最初の typedef _ta = (a) によって、 _ta が式 (a) の型に対する別名となる。 これは普通の typedef の使い方と、実はよく似ていることに気付くだろうか。 これによって、_ta _a = (a)_a という変数を型 _ta、すなわち、 (a) と同じ型であると宣言する。 そして、変数 _a に値 (a) が代入されるわけだ。 この場合、最初の (a) の評価は単に型を決定するのに使われるだけで、実際に実行されるわけではないので、副作用は生じない。

しかし、このようなマクロは逆に、自動的に、適切にキャストしてくれなくなることにも注意して欲しい。 もし double で宣言した変数を int として比較したいときには、

   1   m = max ((int) 1.01, 2);

などと明示的に書かねばならない。だから、やはりマクロというものは十分に気を付けて使わなければならないのである。

ちょっと楽な switch 文

switch 文では、ときどき非常に面倒くさいことをする必要性がある。 例えば、文字列の最初の文字がアルファベットの小文字で始まるか、大文字で始まるか、それ以外、というのを switch を使って書きたいとする。 これを普通に書くと信じ難い状態になる。

   1 switch (str[0])
   2   {
   3   case 'a':
   4   case 'b':
   5   ...
   6   case 'z':
   7     /* The starting character of STR is a lower letter.  */
   8     break;
   9 
  10   case 'A':
  11   case 'B':
  12   ...
  13   case 'Z':
  14     /* The starting character of STR is a upper letter.  */
  15     break;
  16 
  17   default:
  18     /* Otherwise.  */
  19     break;
  20   }

ここでは省略したが、実際には 26 * 2 行を書かなければならない。 狂気の沙汰である。 でも、GCC の case range の機能を使うと簡単に書ける。 私は正直 Perl みたいだと思った。

   1 switch (str[0])
   2   {
   3   case 'a' ... 'z':
   4     /* The starting character of STR is a lower letter.  */
   5     break;
   6 
   7   case 'A' ... 'Z':
   8     /* The starting character of STR is a upper letter.  */
   9     break;
  10 
  11   default:
  12     /* Otherwise.  */
  13     break;
  14   }

ここの ... は文面の上で省略してるわけではなくて、実際にこう書く。 こうすると、a から z まで、A から Z まで、というのをたったの一行だけで済ませられる。 もちろん、これは文字定数に限ってはいない。 整数なら何でも使用可能だ。

関数の属性

本当は変数にも付けられるのだが、ここでは関数のみを説明する。 関数には属性を与えることができる。 これによって特別な呼ばれ方が行われるように指定したり、あるいは、特別なコンパイル上の検査を行わせることができる。

では、おそらくあまり使われてはいないが、非常に便利な属性である、constructordestructor について説明しよう。 これらは多分名前から見当が付くだろうし、 C++ を知ってる人には明白だろう。 constructor は関数が必ず main の前に呼び出されることを保証する。 逆に destructor は関数が必ず main の後に呼び出されることを保証する。

constructor を指定するには次のようにする。

   1 void init (void) __attribute__ ((constructor));

こうすると、わざわざ main の最初に

   1 int
   2 main (int argc, char *argv[])
   3 {
   4   /* Initialize.  */
   5   init ();
   6   /* The real procedure....  */

のように書かなくて済む。 ただし、複数の関数にこの属性が付けられても、それらが呼び出される順番については何も保証されていないので注意して欲しい。 同様に、destructor

   1 void cleanup (void) __attribute__ ((destructor));

のように書く。 これによって、cleanup が最後に呼ばれるようになるので、システムには任せられない類の後処理、例えば、データ構造を自動的にファイルに保存するというような処理をやらせることができる。 これは exit が呼び出されたときにもちゃんと実行されるので、main の最後に書くよりも効果的である。

これら以外にも、constformat など有用な属性があるが、自分でマニュアルを読んで勉強してもらいたい。

その他の拡張

他にもたくさんの機能がある。 特に有用なものとして、大きさゼロの配列や、可変長の配列宣言、入れ子の関数定義、可変初期化式などが挙げられる。 しかし、ここで紹介するにはちょっと深みに入り過ぎることになってしまう。 だから、マニュアルを自分で見てみることを勧める。 GCC のマニュアルは非常に上手く書けており、説明は懇切丁寧、例も豊富にある。

ただマニュアルを読むだけでは非常に分かり辛いものに、インライン・アセンブリがある。 インライン・アセンブリとは、C の中に直接アセンブリを記述する拡張である。 これについては、 GAS の知識が必要不可欠になるので、回を改めて説明しようと思う。

まとめ

今回は GCC でのみ使用できる拡張機能について説明した。 次回は、C から少し離れて、パッケージ・レベルの話をしたいと思っている。 すなわち、 Automake である。 いつもの通り、質問、意見、批判などは okuji at enbug dot org まで送って欲しい。


次の回 / 前の回 / はっきんぐ・うぃず・ぐにゅー に戻る。


Copyright © 1999,2000,2007 Yoshinori K. Okuji

Verbatim copying and distribution of this entire article is permitted in any medium, provided this notice is preserved.

HackingWithGnu/GnuStylePartThree (last edited 2007-03-13 20:48:59 by YoshinoriOkuji)