C言語標準ビルトイン型で関数オーバーロードしてはいけない

Posted on

可変長引数とva_listでオーバーロードしてみた

 Cの標準関数には、可変長引数を取るsprintfと、その引数をva_listで受け取るvsprintfの2つがあります

int snprintf(char *str, size_t size, const char *format, ...);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

 Cにはオーバーロードの仕組みがありませんから、引数の渡し方が違うだけの関数の名前が変わってしまうのもやむなしです。でも、C++ならオーバーロードをサポートしていますから、これらをこんな感じで一緒にできたら嬉しいな~と思いました。

std::string format(const std::string& format, ...);
std::string format(const std::string& format, va_list ap);

 が、しかし。このオーバーロードは64bit環境でのみ、*たまたま*うまく行きますが、32bit環境では失敗します。

 サンプルを使ってしらべましょう

#include <string>
#include <cstdarg>
#include <cstdio>


std::string format(const std::string& format, ...)
{
	printf("FORMAT 1 CALLED\n");
	return ""; /* サンプルなので未実装 */
}
std::string format(const std::string& format, va_list ap)
{
	printf("FORMAT 2 CALLED\n");
	return ""; /* サンプルなので未実装 */
}

int main(){
	va_list list;
	
	format("format: %s", "Hey!");

	return 0;
}

 さて、main内でのformat呼び出しに注目してください。第二引き数はconst char*なので、最初の可変長引数の関数が呼ばれるのを期待したいところです。

 実際、Fedora 16 x64(gcc 4.6.3)でビルドして実行すると

% ./test 
FORMAT 1 CALLED

 パチパチパチ。

 ところがしかし。Windows Vista32bitのMinGW(gcc 4.6.1)やUbuntu 11.04 32bit(gcc 4.5.2)で同じものをビルドすると…。

$ ./test.exe
FORMAT 2 CALLED

 後者が呼ばれてしまいます…な、なんで…?

ビルトイン型の実際の型を調べるには?

 オーバーロードがうまくいっている、という前提で考えて有り得そうなのは、va_listの実際の型が違う…のかもしれません。

 というわけで、調べてみましょう。g++のEオプションを用いると、プリプロセッサをすべて展開できます。

$ g++ -E test.cpp | grep typedef | grep va_list
typedef __builtin_va_list __gnuc_va_list;
typedef __gnuc_va_list va_list;

 むむむ、ヘッダファイルだけでは解決できませんね…。

 そこで、gdbの出番です。ptypeというコマンドを使うことで、実際の型を調べることができます。

 まずは、Fedora 16 64bitでやってみます。

% gdb test 
(gdb) break main
(gdb) run <- mainの一番上でストップします
(gdb) n <- 一行進めないと、スタック上にlistが定義されない
FORMAT 1 CALLED
22		return 0;
(gdb) ptype list <- main関数で定義されているva_list list;の定義を調べる
type = struct typedef __va_list_tag __va_list_tag {
    unsigned int gp_offset;
    unsigned int fp_offset;
    void *overflow_arg_area;
    void *reg_save_area;
} [1]

 というわけで、何かよくわかりませんが、va_listは構造体らしいです。なるほど、それならちゃんと可変長引数の方が呼ばれるのは納得です。

 64bit環境では構造体なら、32bit Windowsだと…。

(gdb) ptype list
type = char *

 期待通り!va_listの本当の型はchar*でした。だからオーバーロードでchar*を渡したらこっちが呼ばれてしまったんですね。

 これらの違いはなぜ起きるのでしょうか?

 おそらく、64ビットと32ビットでの呼出規約の違いに依るのだと思います。32bitのgccでは、引数をすべてスタック上(=アドレスがある)に格納されるため、va_listは単純なchar*で良いのですが、64bitのGCCでは基本的にレジスタ上に置くため、上記のように構造体で管理しているのでしょう。

 こういった事がありますから、型が明確でない場合はオーバーロードに使うのはやめたほうが良いですね…。

Visual Studio 2010だとどうなんのさ?

 同じリポジトリ内に入れておきました。私は32bit版windowsしかないので、そちらで試したところ、

20110325.png

 …どうやら、同じみたいです…。64bit版は知らないので、誰か試したら教えてね。

コメントを残す

メールアドレスが公開されることはありません。

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください