printfとtypedefの微妙な関係 - 無視されがち?なformt warning

型ができていない者が芝居をすると型なしになる。メチャクチャだ。
型がしっかりした奴がオリジナリティを押し出せば型破りになれる。どうだ、わかるか?
立川談志 - 『赤めだか』より

ひさびさにコーディングの話を書きたいと思います。組み込みの現場なんかで結構よく見かける話かと思いますが、以下のようなコードに対してでるwarningのお話です。

printfとtypdefの微妙な関係

ケースとしては、int等の組み込み型が環境依存なのを嫌って、typedefでint32_tやINT32という型におきかえることがあります。例えば以下のコード。
typedef long INT32;
int
main(int argc, char* argv[])
{
  INT32 val = 1;
  printf("val=%d\n", val);
  return 0;
}
さて、これをコンパイルすると、例えばgccなら以下のようなwarningがでます。(-Wallや-Wformatがある場合)
$ gcc -o test.c
test.c:9:22: warning: format specifies type 'int' but the argument has type
      'INT32' (aka 'long') [-Wformat]
  printf("val=%d\n", val);
-Werrorがついていればエラーになりますが、そうでない場合は、 実害なし、というか、別にあってるよね、くらいで無視されがちな気がしています。

実際のところ一般的な32bit処理系であれば、intとlongはどちらも符号付きの32bit整数型なので、 数値計算等で使っている範囲では全く同じなのですが、それでもwarningは出ます。なぜでしょうか?

INT32のようなtypedefを定義する理由は処理系依存の型を直接使わずに、 移植性を高めるために使われるものです。
(上の例なら、longが64bitになる64bitの処理系では、intへのtypedefに変えるなどする) 一方で、printfの%dは、そもそも処理系依存の型であるintを期待するものなので、 そこはintでなければ、処理系によっては不一致を起こすので、32bit系であろうと%dにはlongを渡す必要があるのです。

では、どうですればいいのでしょう?
上の例であれば、%dの代わりに%ld(signed long指定)を用いればwarningはなくなります。あるいは、引数側でintへのcastをしてやれば同じく問題はなくなります。
typedef long INT32;
int
main(int argc, char* argv[])
{
  INT32 val = 1;
  printf("val=%d\n", (int)val);
  return 0;
}
個人的には、printfのようなログ出力は処理系依存なので、引数の型の方にcastする上記やり方がスマートかと思います。 もちろんログなどではなくてファイルフォーマットにかかわるもので、移植時の型サイズ維持が重要なものはformat string側をかえるのが筋がいいものもあるとは思います。

「移植性をあげる」という目的で、処理系依存型を非依存型に変換するのはよく見かけるコードですが、一方で、非依存型から処理系依存型への変換というのは結構ないがしろにされがちです。strcmp等のstring系の関数でchar型を期待している関数を使う時も同じようなことがままあります。

なお、size_t, ssize_tについては、C99から専用のformat指定子zが定義されていますのでそれを使いましょう。(size_tから%zu、ssize_tなら%zd)

無視して良いwarning?

unsed-variablesとformatのwarningは無視されがちなwarningの2大巨頭ではないかと思います。どちらも大抵の場合は、確かに実害はないのですが、ごくまれにバグのものもあります。例えば以下のコード。
const char* name="taro";
printf("%d: name=%s\n", name, strlen(name)); 
引数順間違って%sに数値渡しているのでbuffer overrunをひきおこします。
ここで言いたいのは、「実害なし」という名目でwarningを残したり、 あえて、-Wno-formatのようにwarning抑制したりすることは、こういう本当にバグを埋もれさせかねないので、 基本的にはwarningは全て除去するというのは基本ポリシーとして実施すべきかと思います。

なお、デバッグ用にprintfを自作のマクロや関数でラップすることがありますが、 何もしないとprintfのformatチェックが機能しないことになり、 warning無視と同じくバグに気づくチャンスを逃すことになりかねません。 対策としては、例えばgccでは、独自定義マクロでもformatチェックするようにattributeしていることで コンパイラに指定する機構がありますので、あわせて活用することをおすすめします。
以下はgccの例です。
void debug_printf(const char* format, ...) __attribute__ ((format(printf, 1, 2)\
));

void debug_printf(const char* format, ...)
{
  va_list arg;
  va_start(arg, format);
  vprintf(format, arg);
  vfprintf(stderr, format, arg);
  va_end(arg);
}
上の例だと、__attribute__ formatという属性のを関数に付与することでformatチェックが入るようになります。引数の意味は、関数タイプ(この例だとprintf)、format文字列の位置(この例では1)、最初の引数の位置(この例では2)になっています。詳細は、以下gccのmanualが詳しいです。 ようするに、warningは正しく活用しましょうということですね。

なお、デバッグ出力用の可変個引数マクロやwarningの話は随分前にもとりあげていたので、よろしければご参考までに。

【関連記事】
可変個引数マクロを使う
warningに気を配る
使用しない仮引数

コメント

非公開コメント

本のおすすめ

4873115140

4274065979

4198642257

4274068579

4479791779

B00SIM19YS


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索