既存のCソースコードをC++コンパイラ向けに移植する方法

上司から、既存のCソースコードをC++コンパイラ向けに移植する仕事を任されましたが、どこから手をつけていいのか、どこに注意点があるのか、全くわからないとしましょう。そのような場合に、本稿の内容が役に立ちます。

CからC++へと移行する理由はたくさんあります。本稿では、古いCのアプリケーションをC++使って動作させることに焦点をあてます。ここでは、クラス・コンストラクタ・デストラクタなど、C++の複雑な機能については説明せず、C++コンパイラを使用してCアプリケーションをコンパイルする方法を説明します。

基本的に、C++コンパイラでCアプリケーションをコンパイルし直すことは、それほど難しくはありません。なぜなら、ほとんどのCの文法はC++で有効だからです。しかし、プログラマを悩ますいくつかの危険な違いが存在します。コンパイラが文句を言ってくれるなら簡単です。厄介なのは、 コンパイルは問題なくとも、実は全く異なる振る舞いとなるケースです。

CとC++宣言の比較

C++における宣言は(関数および型の両方)、Cの宣言とは異なります。C++では、クラス・構造体・共用体・列挙型の名前は、自動的に型名となります。下記のCおよびC++の宣言を比較してみましょう。

C code
C++ code
enum fruit {
apple, orange, banana
};
typedef enum fruit fruit;

struct person {
char name[20];
int age;
fruit favorite;
};
typedef struct person person;

void foo() {
person thomas;
}
enum fruit {
apple, orange, banana
};

struct person {
char name[20];
int age;
fruit favorite;
};

void foo() {
person thomas;
}
 

typedefがC++の実装からはなくなっていることがわかります。typedefをCからC++へと移行する際に残すことは問題ありません。コンパイルはとおるでしょう。

自動的に型名(例えば構造体の型名)を取り込んでくれることは、ほとんどの場合、非常に便利です。しかし、軽微なエラーを生じる場合があります。例えば、以下のCソースコードは(恐らく)C++コンパイラを使用した場合にクラッシュするでしょう。

char tone[100];
void dial() {
struct tone {
short number;
char freq[3];
};
...
char* tone_data = (char*) malloc(sizeof(tone));
}

これは、sizeof(tone)はCコンパイラでは100、C++コンパイラでは5となるからです。toneは、Cコンパイラでは配列、C++コンパイラでは構造体型を参照するからです。(一般に、型と変数・フィールドに対して、同じ名称をつけるべきではないですが、それは本稿では触れません。)

Cにおいて明示的に引数の型を指定せず宣言される関数は任意の数の引数を受け取ることができます。C++においては引数がない関数の宣言となります。さらに、Cでは宣言されていない関数を使用することが可能で(ただし、問題のある書き方のため警告は出るでしょう)、C++ではそれは許されません。

void f();
int main() {
f(3); /* Error: too many arguments in function call */
g(19); /* Error: identifier "g" is undefined */
}

適切な型のプロトタイプ宣言はこれらの問題への解説策となります。Cは変数の型の省略や戻り値の型の省略などに寛容で(たとえ、それが問題のある書き方でも)、その場合に暗黙にint型を使用します。一方で、C++は厳密であり、明示的な型の宣言を必要とします。例えば、以下の記述はCではコンパイルできますが、C++ではとおりません。

foo() {         /* Warning: omission of explicit
const v = 12; type is nonstandard */

... /* Warning: omission of explicit
} type is nonstandard */

ポインタの変換

C言語は、異なった型同士の暗黙の型変換について、非常に寛容です。一方、C++は厳密です。Cと対照的に、C++は厳密な型の判別を行います。この話に直面するのは、例えば、下記のコードをコンパイルするときです。

#include <stdlib.h>
int main() {
int* px = malloc(sizeof(int));
/* Error: a value of type "void *" cannot be used to initialize an entity of type "int *" */
}

C++では、mallocよりもnewを好むべきということは別として、上記はそもそもC++では有効な記述ではありません。それは、mallocの戻り値のvoid*型から他の型へのポインタ型(上記であればint*)への暗黙の変換が、C++では許されていないからです。上記をコンパイルできるようにするため、明示的な型のキャストが必要です。

列挙型

列挙型の定数に対する同様のケースがあります。Cでは、int型から列挙型への暗黙の変換が許されます。しかし、C++では禁止されています。そのため、下記はCでは有効(フレンドリーな警告メッセージは出るでしょうが)で、C++では無効です。

enum fruit { apple, orange, banana };
typedef enum fruit fruit_t;
void print(fruit_t i) {
switch(i) {
case apple:
printf("apple"); break;
case orange:
printf("orange"); break;
case banana:
printf("banana"); break;
}
}
 int main() {
int i;
for (i = 0 ; i < 3 ; ++i) {
print(i);
/* Error: argument of type "int" is incompatible with parameter of type "fruit_t" */
}
}

C++では以下のように記述する必要があります。

print((fruit_t) i);

既存のCソースコードをC++コンパイラ向けに移植する際のガイドライン

以下のガイドラインがCで実装済みのアプリケーションをC++コンパイラ向けに移植する際に役立ちます。

  1.  型の命名規則、および変数・フィールドの命名規則には異なった慣例を用いること。例えば、型名であれば、fruit_t・tone_t・range_tなどのように、変数・フィールド名であればfruit・tone・rangeなどとすること。
  2.  広い型(例えばvoid*やint)から狭い型(int*やenum)への型変換については、明示的なキャストを用いること。
  3. 使用する全ての関数のプロトタイプ宣言をすること。
  4. 使用している型を明示すること。戻り値や変数について、暗黙に決められた型を信用しないこと。たまたま、うまくいくこともあるでしょうが、潜在的に小さな問題をはらみます。