構造体の定義のネストで移植性の壁に衝突した話。extern "C" にも限界はあるのさ……。

C90相当のC言語で実装したライブラリがありまして、C++でアプリを作る際にこいつをちょろっと使っちまおうと考えたわけですな。

ところがそのライブラリのヘッダファイルにこんな感じの構造体の定義が書かれていたのです。なんてことでしょう。

/* test.h */

#ifndef TEST_H_
#define TEST_H_ 

typedef struct AA AA;
struct AA {
	int a;
	struct BB {
		int b;
	} bb;
	int c;
};

typedef struct BB BB;

#endif /* ndef TEST_H_ */

このヘッダをC言語のソースでインクルードして、ネストの内側の構造体 BB の型を使うのは問題なくて、

#include <stdio.h>
#include "test.h"

int main(void)
{
	BB bb;

	bb.b = 1;
	(void) printf("%p: %p: %d\n", &bb, &bb.b, bb.b);

	return 0;
}

このソースはTDM-GCC 4.5.1、Visual C++ 2005/2010で問題なくビルドできるし、実行しても大丈夫。

しかし悲しいかな、C++のソースで同じことを試みると……、

#include <stdio.h>
extern "C" {
#include "test.h"
}

int main()
{
#if 1
	BB bb;
#else
	struct BB bb;
#endif

	bb.b = 1;
	(void) printf("%p: %p: %d\n", &bb, &bb.b, bb.b);

	return 0;
}

3つのコンパイラの何れもビルドエラー。

TDM-GCC 4.5.1 (g++ tdm-1 4.5.1) :

test.cpp: In function 'int main()':
test.cpp:9:5: error: aggregate 'BB bb' has incomplete type and cannot be defined

Visual C++ 2005 (Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 14.00.50727.762 for 80x86) :

test.cpp(9) : error C2079: 'bb' が 未定義の struct 'BB' で使用しています。
test.cpp(14) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。
test.cpp(15) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。
test.cpp(15) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。

Visual C++ 2010 (Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86) :

test.cpp(9) : error C2079: 'bb' が 未定義の struct 'BB' で使用しています。
test.cpp(14) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。
test.cpp(15) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。
test.cpp(15) : error C2228: '.b' の左側はクラス、構造体、共用体でなければなりません
        型は 'int' です。

この件について「CとC++では構造体タグの有効範囲が〜〜」という指摘(C++では struct BB ではなく struct AA::BB だ)は微妙にお門違いで、というのもこのコードを書いた人も今回のビルドエラーに遭遇した私もその問題は認識していたからだ。

ただ詰めが甘いことに「『extern "C"』を付けてヘッダをインクルードしているのに何故 BB でOKにならないのか」と悩んでいたという……。「extern "C"」はリンク時の関数名の非互換性の問題を解決する為の方法なので、コンパイル時の構造体タグの有効範囲の問題には効果がない。今さら気づいた話だけど。

この問題のいやらしい所は、例えばC言語で書かれた別のライブラリのヘッダにこんな内容が書かれていて、

#ifndef TEST2_H_
#define TEST2_H_ 

/* 先ほどの test.h を使う */
#include "test.h"

typedef struct CC CC;
struct CC {
	double a;
	BB bb;
};

#endif /* ndef TEST2_H_ */

このヘッダをC++のソースでインクルードする場合。先ほどの例ではC++のコードにて型名を BB から AA::BB に変更すれば問題を回避できるけど、このヘッダの場合はその方法が使えない(Cで書いたライブラリのヘッダなので AA::BB に変更したら文法的にNG)。

一番良い方法は、test.hにてstruct BBの定義をstruct AAの中から出してしまうことだ。

C言語C++は全く別物だ。ある程度互換性は高いけど別物だ。WikipediaのC++の項のような、

C++は基本的にC言語の上位互換であるが、厳密には異なる。

だとか、

C言語では正当でもC++では不正になる部分や、C++とは動作が異なる部分が若干存在する。

といった記述を見ると何故か大した問題ではない(非互換の部分はレアケースだ)と思ってしまうのだけど、実際にはCプログラマが普段何気なく記述している部分に問題が潜んでいることが多いので、C++への移植性を考慮したコードを書く人は普段から注意しておかなくてはならない。

そんなこともあって、個人的にC言語C++について「互換性が高い」という言い方はしたくない。アレはちょっと似ているだけで全く別物の言語なのさ……。