
ソースコードを一度でも読んだことがある誰もが目にしたことのあるプリプロセッサディレクティブ。例えば、ソースコードの始めにある#includeなどです。プリプロセッサは、コンパイラがコンパイルを開始する前に、ソースをディレクティブに従って書き換えるためのものです。便利なツールである一方、思わぬ間違いを引き起こすことがあります。
この記事では、プリプロセッサの基礎をご紹介します。加えて、マクロおよび#includeディレクティブ・条件付きコンパイル・#errorおよび#pragmaという特別なディレクティブをご紹介します。
Includeディレクティブ
最もシンプルなディレクティブは#includeです。プリプロセッサがこのディレクティブを見つけた時、その場所で、指定されたファイルをオープンし、中身を挿入します。
#includeディレクティブは2つの形式があります。
#include <systemfile.h>
#include "myfile.h"
1つ目の形式は、stdio.hのような標準ヘッダをインクルードするのに使われます。2つ目の形式は、ユーザがアプリケーションに自作のヘッダをインクルードするためのものです。
マクロ(プリプロセッサの最も基本的な使い方)
プリプロセッサの最も便利な使い方の一つは、ユーザがマクロを定義することです。マクロディレクティブはソースコードの一部をマクロとして識別するためのものです。プリプロセッサがマクロを発見すると、マクロをその定義に置換します。
マクロには、オブジェクト形式と関数形式の2種類があります。違いは、関数形式マクロが引数を持つことです。
慣習により、マクロ名は大文字で記述します。
#defineディレクティブは、マクロを定義するために使用します。下記の例は、定数2に対して、NUMBER_OF_PLAYERSというマクロを定義しています。これはオブジェクト形式マクロの例です。
#define NUMBER_OF_PLAYERS 2
int current_score[NUMBER_OF_PLAYERS];
関数形式マクロの例は、以下で、PRINT_PLAYERとして、より複雑なソースコードを定義しています。
#define PRINT_PLAYER(no) printf("Player %d is named %s", no, names[no])
マクロの定義は、1行で記述する必要があります。C言語標準では、複数行を定義するには、文末をバックスラッシュでつなぐことで可能です。例えば以下のようになります。
#define A_MACRO_THAT_DOES_SOMETHING_TEN_TIMES \
for (i = 0; i < 10; ++i)\
{ \
do_something(); \
}
加えて、ユーザは、C言語標準が提供するたくさんの定義済みマクロが利用できます。例えば、__FILE__という定義済みのマクロは、ソースコードのファイル名の文字列として使用できます。
マクロの定義を取り消したい場合は、#undefディレクティブを使用します。
オブジェクト形式マクロ
オブジェクト形式マクロは、識別子をソースコード中で置換するために使用します。
通常、このマクロは、定数を宣言するために使います。これにより、定数がソースコード上で読みやすくなります。
#define SQUARE_ROOT_OF_TWO 1.4142135623730950488016887
double convert_side_to_diagonal(double x)
{
return x * SQUARE_ROOT_OF_TWO;
}
double convert_diagonal_to_side(double x)
{
return x / SQUARE_ROOT_OF_TWO;
}
プリプロセッサマクロは、識別子をソースコードの断片に置き換えるというだけなので、トリッキーなことにも使用できます。例えば、以下もコンパイル可能なソースコードとなります。
#define BLA_BLA );
int test(int x)
{
printf("%d", x BLA_BLA
}
関数形式マクロ
関数形式マクロは、引数を取ります。通常の関数呼び出しのような記述になり、以下が例です。
#define SEND(x) output_array[output_index++] = x
関数形式マクロをプリプロセッサが見つけた時、マクロの定義に置換します。マクロの引数は、マクロをソースコード中の参照している箇所で用いている実引数に置換されます。
SEND(10)
例えば、上記の記述に対して、コンパイラは以下の記述とみなします。
output_array[output_index++] = 10
次に、マクロについて、間違えやすいポイントを紹介します。
条件付きコンパイル
プリプロセッサの最も強力な機能の一つが条件付きコンパイルです。これにより、条件に基づいて、ソースコードの一部をコンパイルの対象外とすることが可能です。
例えば、あなたのソースコードの一部がArmアーキテクチャ向けであるならば、条件付きコンパイルを使い、他のアーキテクチャ向けのコンパイル時には対象外とすることができます。
プリプロセッサディレクティブは、#ifdef、#ifndef、#if、#elif、#elseがあり、コンパイルを制御します。#ifdef(および#ifndef)ディレクティブは、識別子が定義(または未定義)されているかどうかによって、取り込むソースコードを変えます。
#ifdef ARM_BUILD
__ARM_do_something();
#else
generic_do_something();
#endif
#ifディレクティブは、整数と論理演算を扱うことができます。
#if (NUMBER_OF_PROCESSES > 1) && (LOCKING == TRUE)
lock_process();
#endif
#elifディレクティブは、#elseと#ifの両方のディレクティブをつなげたように動きます。
#ifと#elifディレクティブは、特別な演算子definedを組み合わせて、識別子が定義されているかをチェックし、以下のような複雑な条件で使用できます。
#if defined(VERSION) && (VERSION > 2)
...
#endif
インクルードガード
条件付きコンパイルの典型的な使い方の一つは、同じファイルが一度しかインクルードされないことを確実にすることです。これはインクルードガードと呼びます。コンパイルの時間を短くするだけでなく、同じヘッダファイルを2度インクルードすることで多重宣言などのエラーを出すことを抑制します。
インクルードガードは以下のような例になります。
#ifndef MYHEADER_H
#define MYHEADER_H
/* ここにヘッダファイルの中身が挿入されます。 */
#endif
ヘッダファイルが取り込まれる初回は、MYHEADER_Hという識別子が定義されていないので、定義され、そしてヘッダファイルの中身が取り込まれます。2回目以降は、MYHEADER_Hが定義済みのためにヘッダファイルの中身は挿入されません。
エラーディレクティブ
#efforディレクティブは、コンパイラにエラーメッセージを発生させるものです。これは、矛盾するマクロの定義などを検出し、コンパイルの一貫性をチェックするのに便利です。
#if USE_COLORS && !HAVE_DISPLAY
#error "You cannot use colors unless you have a display"
#endif
プラグマディレクティブ
最後に#pragmaディレクティブを紹介します。このディレクティブは、プログラマがコンパイラの振る舞いを制御し、またコンパイラベンダにC言語の仕様を超えた独自の実装をする機会を提供します。