第9章: プリプロセッサ
9.1 プリプロセッサの理論
プリプロセッサとは
プリプロセッサ(Preprocessor)は、コンパイル前にソースコードを変換するツールである。
歴史的背景:
- 1972年、Alan Snyderによって最初のCプリプロセッサが実装
- 当初はアセンブラのマクロ機能を参考に設計
- 後にCの言語仕様の一部として標準化
ソースコード処理の流れ:
ソースファイル (.c)
↓
プリプロセッサ ← #include, #define, #ifdef などを処理
↓
翻訳単位 (.i) ← 純粋なC言語コード
↓
コンパイラ
↓
オブジェクトファイル (.o)
プリプロセッサの役割
プリプロセッサはテキスト変換を行う:
#include#define#if, #ifdef#line#error, #warning#pragma9.2 ファイルインクルード
#include ディレクティブ
/* システムヘッダ: 標準の検索パスを使用 */
#include <stdio.h>
#include <stdlib.h>
/* ユーザーヘッダ: カレントディレクトリから検索 */
#include "myheader.h"
#include "path/to/header.h"
検索パスの違い
#include <header.h>:
1. コンパイラの標準インクルードパス
/usr/include
/usr/local/include
など
#include "header.h":
1. カレントディレクトリ
2. -I オプションで指定されたパス
3. 標準インクルードパス(フォールバック)
インクルードガード
/* header.h */
#ifndef HEADER_H
#define HEADER_H
/* ヘッダの内容 */
struct Point {
int x, y;
};
void process_point(struct Point *p);
#endif /* HEADER_H */
#pragma once(非標準だが広く使用)
/* header.h */
#pragma once
/* ヘッダの内容 */
/* インクルードガードと同じ効果 */
インクルードガードの重要性
/* 問題: 多重インクルード */
/* main.c */
#include "a.h" /* a.h が b.h をインクルード */
#include "b.h" /* b.h が再度読み込まれる */
/* b.h にインクルードガードがないと:
- 同じ定義が2回現れる
- コンパイルエラーまたは未定義動作 */
9.3 マクロ定義
オブジェクト形式マクロ
/* 定数の定義 */
#define PI 3.14159265358979
#define MAX_BUFFER_SIZE 1024
#define VERSION "1.0.0"
/* 複数行マクロ(バックスラッシュで継続) */
#define LONG_STRING "This is a very long string that \
spans multiple lines in the source code"
/* 空のマクロ */
#define DEBUG /* 値なし、存在のみをチェック */
関数形式マクロ
/* 基本的な関数形式マクロ */
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define MIN(a, b) ((a) < (b) ? (a) : (b))
/* 使用 */
int result = SQUARE(5); /* ((5) * (5)) → 25 */
int m = MAX(10, 20); /* ((10) > (20) ? (10) : (20)) → 20 */
マクロの括弧の重要性
/* 悪い例: 括弧なし */
#define SQUARE_BAD(x) x * x
int a = SQUARE_BAD(3 + 2); /* 3 + 2 * 3 + 2 = 11(期待は25) */
/* 良い例: 括弧あり */
#define SQUARE_GOOD(x) ((x) * (x))
int b = SQUARE_GOOD(3 + 2); /* ((3 + 2) * (3 + 2)) = 25 */
二重評価問題
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = MAX(x++, 3); /* x++ が2回評価される可能性 */
/* 展開: ((x++) > (3) ? (x++) : (3)) */
/* x の値が予測困難 */
/* 解決策1: GCC拡張 */
#define MAX_SAFE(a, b) ({ \
__typeof__(a) _a = (a); \
__typeof__(b) _b = (b); \
_a > _b ? _a : _b; \
})
/* 解決策2: インライン関数を使う */
static inline int max_int(int a, int b)
{
return a > b ? a : b;
}
特殊な演算子
/* # 演算子: 文字列化 */
#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)
printf("%s\n", STRINGIFY(hello)); /* "hello" */
#define VERSION_MAJOR 1
printf("%s\n", TOSTRING(VERSION_MAJOR)); /* "1" */
/* ## 演算子: トークン連結 */
#define CONCAT(a, b) a ## b
#define MAKE_VAR(n) var_ ## n
int MAKE_VAR(1) = 10; /* int var_1 = 10; */
int CONCAT(my, var) = 20; /* int myvar = 20; */
可変引数マクロ(C99)
/* __VA_ARGS__: 可変引数を展開 */
#define DEBUG_PRINT(fmt, ...) \
printf("[DEBUG] " fmt "\n", __VA_ARGS__)
DEBUG_PRINT("x = %d, y = %d", 10, 20);
/* → printf("[DEBUG] " "x = %d, y = %d" "\n", 10, 20); */
/* ##__VA_ARGS__: 引数がない場合のカンマを削除(GCC拡張) */
#define LOG(fmt, ...) \
printf(fmt, ##__VA_ARGS__)
LOG("Hello"); /* printf("Hello"); カンマが削除される */
/* C23: __VA_OPT__ */
#define LOG_C23(fmt, ...) \
printf(fmt __VA_OPT__(,) __VA_ARGS__)
事前定義マクロ
/* 標準の事前定義マクロ */
__FILE__ /* 現在のファイル名 */
__LINE__ /* 現在の行番号 */
__DATE__ /* コンパイル日 "Mmm dd yyyy" */
__TIME__ /* コンパイル時刻 "hh:mm:ss" */
__func__ /* 現在の関数名(C99) */
__STDC__ /* 標準Cコンパイラなら1 */
__STDC_VERSION__ /* C標準のバージョン */
/* 使用例: デバッグ出力 */
#define DEBUG_LOG(msg) \
fprintf(stderr, "%s:%d (%s): %s\n", \
__FILE__, __LINE__, __func__, msg)
void my_function(void)
{
DEBUG_LOG("Entering function");
/* 出力: main.c:42 (my_function): Entering function */
}
マクロの定義解除
#define TEMP 100
/* TEMP を使用 */
#undef TEMP
/* TEMP は未定義 */
/* 再定義(警告なしに)*/
#define TEMP 200
9.4 条件付きコンパイル
基本構文
#if expression
/* expression が真のときコンパイル */
#elif expression2
/* expression2 が真のときコンパイル */
#else
/* どれも真でないときコンパイル */
#endif
#ifdef と #ifndef
/* マクロが定義されているか */
#ifdef DEBUG
printf("Debug mode\n");
#endif
/* マクロが定義されていないか */
#ifndef RELEASE
printf("Not release mode\n");
#endif
/* 同等の書き方 */
#if defined(DEBUG)
/* ... */
#endif
#if !defined(RELEASE)
/* ... */
#endif
複雑な条件
/* 論理演算子 */
#if defined(LINUX) && defined(X86_64)
/* Linux 64bit */
#elif defined(LINUX) && defined(ARM)
/* Linux ARM */
#elif defined(WINDOWS)
/* Windows */
#else
#error "Unsupported platform"
#endif
/* 値の比較 */
#if __STDC_VERSION__ >= 201112L
/* C11以降 */
#define HAVE_STATIC_ASSERT 1
#elif __STDC_VERSION__ >= 199901L
/* C99 */
#define HAVE_STATIC_ASSERT 0
#endif
プラットフォーム判定
/* コンパイラ判定 */
#if defined(__GNUC__)
#define COMPILER "GCC"
#elif defined(__clang__)
#define COMPILER "Clang"
#elif defined(_MSC_VER)
#define COMPILER "MSVC"
#else
#define COMPILER "Unknown"
#endif
/* OS判定 */
#if defined(__linux__)
#define OS "Linux"
#elif defined(__APPLE__)
#define OS "macOS"
#elif defined(_WIN32)
#define OS "Windows"
#else
#define OS "Unknown"
#endif
/* アーキテクチャ判定 */
#if defined(__x86_64__) || defined(_M_X64)
#define ARCH "x86_64"
#elif defined(__i386__) || defined(_M_IX86)
#define ARCH "x86"
#elif defined(__aarch64__)
#define ARCH "ARM64"
#elif defined(__arm__)
#define ARCH "ARM"
#else
#define ARCH "Unknown"
#endif
デバッグ機能の制御
#ifdef NDEBUG
/* リリースモード */
#define ASSERT(expr) ((void)0)
#define DEBUG_ONLY(code)
#else
/* デバッグモード */
#define ASSERT(expr) \
((expr) ? (void)0 : \
(fprintf(stderr, "Assertion failed: %s at %s:%d\n", \
#expr, __FILE__, __LINE__), abort()))
#define DEBUG_ONLY(code) code
#endif
/* 使用 */
ASSERT(ptr != NULL);
DEBUG_ONLY(printf("Debug: ptr = %p\n", ptr);)
9.5 その他のディレクティブ
#error と #warning
/* コンパイルを停止 */
#if !defined(VERSION)
#error "VERSION must be defined"
#endif
/* 警告を出力(GCC/Clang拡張) */
#if BUFFER_SIZE < 256
#warning "BUFFER_SIZE is very small"
#endif
#line
/* 行番号とファイル名を変更 */
#line 100 "generated_code.c"
/* 以降のコードは generated_code.c の100行目として報告される */
/* 主な用途: コードジェネレータ */
#pragma
/* コンパイラ固有の指示 */
/* GCC: 警告の制御 */
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_var;
#pragma GCC diagnostic pop
/* 構造体のパッキング */
#pragma pack(push, 1)
struct Packed {
char a;
int b;
};
#pragma pack(pop)
/* 標準化されたプラグマ(C99) */
#pragma STDC FP_CONTRACT ON
#pragma STDC FENV_ACCESS OFF
#pragma STDC CX_LIMITED_RANGE ON
_Pragma 演算子(C99)
/* マクロ内でプラグマを使用 */
#define DO_PRAGMA(x) _Pragma(#x)
#define DISABLE_WARNING(name) \
DO_PRAGMA(GCC diagnostic ignored #name)
DISABLE_WARNING(-Wunused-variable)
/* 展開: _Pragma("GCC diagnostic ignored \"-Wunused-variable\"") */
9.6 マクロの設計パターン
ラッパーマクロ
/* システムコールのラッパー */
#define SAFE_CLOSE(fd) do { \
if ((fd) >= 0) { \
close(fd); \
(fd) = -1; \
} \
} while (0)
/* メモリ管理のラッパー */
#define SAFE_FREE(ptr) do { \
free(ptr); \
(ptr) = NULL; \
} while (0)
do-while(0) イディオム
/* なぜ do-while(0) を使うのか */
/* 悪い例: 中括弧のみ */
#define BAD_MACRO(x) { stmt1; stmt2; }
if (condition)
BAD_MACRO(x); /* セミコロンの後に空文が入る */
else
other(); /* 構文エラーの可能性 */
/* 良い例: do-while(0) */
#define GOOD_MACRO(x) do { stmt1; stmt2; } while (0)
if (condition)
GOOD_MACRO(x); /* セミコロンが自然に収まる */
else
other(); /* 正常にコンパイル */
コンパイル時アサート
/* C11以前 */
#define STATIC_ASSERT(expr, msg) \
typedef char static_assertion_##msg[(expr) ? 1 : -1]
STATIC_ASSERT(sizeof(int) == 4, int_must_be_4_bytes);
/* C11以降 */
#include <assert.h>
static_assert(sizeof(int) == 4, "int must be 4 bytes");
X-Macro
/* X-Macroパターン: コードの重複を避ける */
/* 定義ファイル */
#define COLOR_LIST \
X(RED, 0xFF0000) \
X(GREEN, 0x00FF00) \
X(BLUE, 0x0000FF)
/* enum定義 */
enum Color {
#define X(name, value) COLOR_##name,
COLOR_LIST
#undef X
COLOR_COUNT
};
/* 文字列配列 */
const char *color_names[] = {
#define X(name, value) #name,
COLOR_LIST
#undef X
};
/* 値配列 */
int color_values[] = {
#define X(name, value) value,
COLOR_LIST
#undef X
};
汎用マクロ(C11 _Generic)
/* 型に応じた関数選択 */
#define print_value(x) _Generic((x), \
int: print_int, \
double: print_double, \
char *: print_string, \
default: print_unknown \
)(x)
void print_int(int x) { printf("%d\n", x); }
void print_double(double x) { printf("%f\n", x); }
void print_string(char *x) { printf("%s\n", x); }
void print_unknown(void *x) { printf("unknown\n"); }
/* 使用 */
print_value(42); /* print_int(42) */
print_value(3.14); /* print_double(3.14) */
print_value("hello"); /* print_string("hello") */
9.7 プリプロセッサの落とし穴
名前空間の汚染
/* 悪い例: 一般的な名前 */
#define MAX 100
#define min(a, b) ((a) < (b) ? (a) : (b))
/* 良い例: プレフィックスを付ける */
#define MYLIB_MAX 100
#define MYLIB_MIN(a, b) ((a) < (b) ? (a) : (b))
マクロと演算子の優先順位
/* 悪い例 */
#define ADD(a, b) a + b
int x = ADD(1, 2) * 3; /* 1 + 2 * 3 = 7(期待は9) */
/* 良い例 */
#define ADD(a, b) ((a) + (b))
int y = ADD(1, 2) * 3; /* ((1) + (2)) * 3 = 9 */
マクロと副作用
/* 引数に副作用があると問題 */
#define ABS(x) ((x) < 0 ? -(x) : (x))
int n = -5;
int result = ABS(n++); /* n が複数回評価される */
/* 安全な代替 */
static inline int abs_safe(int x)
{
return x < 0 ? -x : x;
}
文字列リテラルの連結
/* プリプロセッサは隣接する文字列を連結する */
printf("Hello, " "world!\n"); /* "Hello, world!\n" */
/* マクロでの利用 */
#define LOG_PREFIX "[LOG] "
printf(LOG_PREFIX "Message: %s\n", msg);
/* → printf("[LOG] " "Message: %s\n", msg); */
/* → printf("[LOG] Message: %s\n", msg); */
9.8 プリプロセッサ出力の確認
# プリプロセス結果のみを出力
gcc -E source.c -o source.i
# マクロ展開を確認
gcc -E -dM source.c # 定義されている全マクロを表示
# インクルードパスを確認
gcc -v -E source.c
9.9 まとめ
本章では、Cプリプロセッサについて学んだ:
次章では、入出力について学ぶ。
---