C言語における暗黙の型変換とAPI設計

前口上の,つづき.

C言語の整数型に潜む悪夢

まずは,よく知られているところから.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
  int a = 65535;
  char b;

  b = a;

  printf("%d %d\n", a, b);

  return EXIT_SUCCESS;
}

「a に 65535 を代入し,b に a の値を代入しているのだから,b も 65535 になるはず.」 などとLL言語(JavaScript や Ruby など)に慣れていると思ってしまいがちだが,そうはならない.

なぜなら,一般に,char 型の変数が保持できる値の範囲は,int 型の変数が保持できる値の範囲よりも小さいから. 概ね(…とボカす理由は後述),char は -128 から 127 までの整数しか保存できない.

この性質は,ときどき,極めて恐ろしい. C言語は,上の例のように保持できる値の範囲が小さい変数への代入を,エラーとして扱わない.しかしその結果は,おそらく,プログラマの期待とは異なる. こういう挙動によるバグは,発見するのが困難なものとなる.

もしかしたら気の利いた処理系では警告を出してくれるかもしれないが. しかし,私の手元にあった XCode の gcc では,割と厳格な警告を出すようオプションとして -pedantic -std=c99 -W -Wall を指定したが,素通りした.

(2014-Mar-20 補足: 上記,ちょっと筆が滑っている.ダウンキャストへの警告についてはこちらのエントリもご参照頂きたい)

さて,それでは変数 b の型を int 型にすれば解決か? 残念ながら,それもダメ.なぜなら,int 型が保持できる整数の範囲は処理系定義であるから. int 型のサイズが 16 ビットだったなら,保持できる整数の範囲は -32768 から 32767 までしかない.a への代入の時点で,数値が期待と異なる. つまり,上記のリストは,ある環境では動くかもしれないが,別の環境では動かない.

さらに疑心暗鬼になると,char 型のサイズも実はC言語仕様は決めていない. だからもしかすると,int 型と char 型のサイズが共に17ビット以上ある処理系なら,上記のリストは期待通りに動くかもしれない. DSP など特殊用途のプロセッサでは,char 型が 24 ビットだったり 32 ビットだったりというのは,十分に有り得る.

このように,学校などで無邪気に語られる「C言語には移植性がある」などというのは嘘っぱちも甚だしい. C言語プログラマは,細心の注意をもって,自ら移植性を担保しなければならない.

さて,上記をまとめると,C言語プログラミングには,次のような鉄則がある.

整数型変数の代入の際には,右辺にある数値が,左辺にある変数の型に収まるかどうかを,しつこいほど気にしなければならない.

以上が前提知識.本題に入る.

移植性が確保できる C 言語 API

C 言語 API は,(プリプロセッサマクロで書けなくも無いが)普通はC言語の関数として用意する.

C 言語の関数呼び出しは,値渡しだ.ポインタ渡しも,ポインタの値を渡している. つまり,関数呼び出しにおいて,代入と同じことが起こる.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>

int
foo(char b)
{
  printf("%d\n", b);
}

int
main(void)
{
  int a = 65535;

  foo(a);

  return EXIT_SUCCESS;
}

このリストも,エラーも(GCCでは警告も)出ずにリンクまで通る. 代入に関する話題から判る通り printf の出力は,65535 にはならないかもしれないし,なるかもしれない.

ここで C言語での API 設計における鉄則が見えてくる.

APIでは,暗黙の型変換を誘ってしまうような,関数宣言をしてはいけない.

この鉄則を守る方法として,安直なのは,API が受け取る整数型を大きめにとっておくこと. ダウンキャストでは値が落ちるが,アップキャスト(大きな範囲を取れる型へのキャスト)では整数が確実に代入できることは保証されている. しかし,”安直”である理由が2つあり,お薦めはしない.

まず,大きな整数型というのは,コストが高いから. CPUから見て計算コストが高い.メモリも余計に消費する.

そして,古めのC言語仕様では,最大の数値型が何なのか実は判らないから. C99標準以降,処理系が扱える最大の整数型 uintmax_t ならびに intmax_t という型が規定された.しかし,分野によっては無視できないシェアを誇る Microsoft Visual C++ は,C99 への対応が中途半端で,umaxint_t, intmax_t は,定義されていない.

そんなわけで,API は,関数呼び出し時に,整数型のアップキャストもダウンキャストも,させてはいけない.

暗黙の型変換が無いとC言語のコーディングは酷く窮屈になる. だから,C言語仕様も,容認している. しかし,こと API についていえば,暗黙の型変換は,害悪でしかない.

結論として,APIでは,ユーザの関数呼び出し時に使うであろう型を,そのまま用いる,というのが唯一無二の選択となる.

さて,長くなった. Matz 氏と私が,mruby の API についてどのように考えたのか. それは明日 以降に.