Go言語のPrintf関数は %d
や %s
などのフォーマット指定を使って第二引数以降の値を埋め込んだ文字列を出力します。
また、 %v
というどんな型に対してもいい感じに出力してくれる万能なフォーマット指定子もあります。
このPrintf関数はC言語のprintf関数が元になっていますが、元ネタのprintfには %v
という便利なフォーマット指定子がなく、型の種類毎に別々のものを使う必要があります。
なぜ、C言語には %v
相当のものが存在しないのでしょうか?
答え
Goのinterface{}は元の型情報を保持しているが、C言語のva_listは元の型情報を保持していないため。
まず、GoのPrintfのシグネチャを確認します。
以下によると、func Printf(format string, a ...any) (n int, err error)
というシグネチャであることが分かります。
第二引数以降はany型という型で受け取っています。
このany型はinterface{}型と等価であるように定義されています。
そのため、Printf関数は第二引数以降で任意の型を受け取ることができます。
Printf関数がaを受け取った直後ではinterface{}型ですが、文字列に変換するまでのどこかのタイミングでintやstringなどの具体的な型に変換する必要があります。
そのため、以下のようなコードで型switchを行っています。
switch f := arg.(type) {
case bool:
p.fmtBool(f, verb)
case float32:
p.fmtFloat(float64(f), 32, verb)
case float64:
p.fmtFloat(f, 64, verb)
// 以下略
このような処理を行えるということは、Goではinterface{}型にアップキャストした後も元の型が何であったのかという情報を実行時まで保持していることが分かります。
そのため、%vというフォーマット指定子が渡されたとしても、元の型情報を使った適切なダウンキャストが実現できます。
一方でC言語の場合はどうでしょうか?
C言語のprintf関数は以下のシグネチャを持ちます。
int printf(const char *format, ...);
第二引数以降の型情報が見つかりません。
...
で受け取った引数は、 va_*
というマクロを用いてアクセスします。
va_listが...の情報を表しており、va_argが、 ...
から引数を順番に取り出すためのマクロです。
このマクロの第二引数に型を渡すことで、その型として値を取り出すことができます。
va_list l;
va_args(l, int); /* int型として取り出す */
一見するとGoの型アサーションのような仕組みにも見えますが、元の型と異なる型として取り出そうとしたときの挙動が大きくことなります。
Goでinterface{}型をダウンキャストするときに、互換性の無い型にキャストする場合にはエラーが返されるかpanicが発生します。
一方でC言語ではそのような場合の動作は未定義です。多くの処理系ではでたらめなデータが返されることが多いです。
そのため、安全にダウンキャストできるかという確認が実行時にできず、Goのような型によるswitchも不可能です。
そのため、Goの%vのような任意の型に対して使えるフォーマット指定子はCのprintfには存在しません。