先頭へ戻る

プリプロセス | Programming Place Plus 新C++編

Programming Place Plus トップページ新C++編

先頭へ戻る

このページの概要

このページでは、プリプロセスについて説明します。プリプロセスで実行される機能として、ここまでのページでも #include を説明していますが、ほかにも多数の機能が存在しています。このページで、残りのほとんどの機能を取り上げることにします。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



プリプロセス

前のページで #include について解説したとき、ビルドの手順の中に、プリプロセスという段階があることに触れました。プリプロセスは、コンパイルの手前の段階で、プリプロセッサというソフトウェアによって行われます。また、プリプロセスを終えたあとのソースファイルを翻訳単位と呼ぶのでした。コンパイルは翻訳単位に対して行われます

ソースファイルの中で、プリプロセスで処理される箇所のコードは、行の先頭が # で始まるという共通点があります。これはプリプロセッサに与える命令であることを表しており、プリプロセッサディレクティブ(プリプロセッサ指令) (preprocessing directive) と総称されます。

普通、#include のように書きますし、発音するときにも「シャープインクルード」と呼ぶことが多いですが、実は #include は分離できます(つまり空白が挟まっても構わない)。# で始まる行がプリプロセスで処理される行であり、include はプリプロセッサが解釈できる命令の名前です。

マクロ (#define)

マクロ (macro) は、プリプロセスの段階で、ソースコード上の文字の並びを、ほかの文字の並びに置換する機能です。

マクロは #define というプリプロセッサディレクティブを使って定義しますが、以下のように2種類の方法があります。

#define マクロ名 置換後の文字の並び
#define マクロ名(仮引数の並び) 置換後の文字の並び

1つ目はオブジェクト形式マクロ (object-like macro)、2つ目は関数形式マクロ (function-like macro) と呼ばれます。それぞれの詳しい解説はあとで行うことにして、まず共通する話をしておきます。


まず、マクロ定義の行末にはコメントを書いても構いません。

【上級】コメント部分を無視する(1つの空白文字に置き換える)タイミングは、プリプロセッサディレクティブが実行される手前のステップなので、プリプロセスに影響はありません。3

「置換後の文字の並び」を複数行にしたい場合がありますが、普通に改行することは許されず、行末(本物の改行文字の直前)に \ を置く必要があります。

#define INIT_POS(pos)  pos.x = 0; \
                       pos.y = 0; \
                       pos.z = 0;

マクロの効果は、#define を記述した場所から、翻訳単位の終わりまで及びます(あるいは後述する #undef が現れるまで)。その間にある「マクロ名」と同じ記述が、「置換後の文字の並び」へと置換されます

「置換後の文字の並び」は省略できます。これは後述する #if の条件式の中で使用する目的のマクロを定義するときによく取られる方法です。

ヘッダファイルでマクロの定義を行うと、インクルードした側にまで影響が及ぶことに注意してください。予想外のところでソースコードが置換されることになり、トラブルの原因になりえます。マクロ名はすべて大文字とし、すべて大文字の名前はマクロ以外で使わないようにするというガイドラインにしたがうと、トラブルを防げます1

「仮引数の並び」「置換後の文字の並び」がそれぞれまったく同じであれば、同じ名前のマクロ定義が重複しても許されます。違いがある場合はエラーになります2

マクロの定義は、ソースファイル上に記述するだけでなく、コンパイラのオプションとして与える方法もあります。Visual Studio での方法は、「Visual Studio編>プリプロセスで使うマクロを定義する」のページを参照してください。

マクロはかなり強烈な機能であり、危険な面も大きいため、原則としては使わないことを勧めます。ヘッダファイルで定義すると、インクルードされることによって影響範囲が広がるため、特に避けるべきです。ほとんどのケースでは、オブジェクト形式マクロは constexpr変数で代替できますし、関数形式マクロは関数テンプレート(未解説)で代替できます。

マクロの効果を打ち切る(#undef)

マクロの効果は、#undef によって終了させられます。

#unbef マクロ名

「マクロ名」と同じ名前の #define の効果が、この記述を書いたところで終了されます。

オブジェクト形式マクロ

オブジェクト形式マクロは、マクロ定義よりも後ろにある「マクロ名」と同じ文字の並びを、「置換後の文字の並び」に置き換えます。

#define CARD_MARK_NUM       4   // マークの総数
#define EACH_MARK_CARD_NUM  13  // 各マークのカードの枚数
#define TRUMP_CARD_NUM      (EACH_MARK_CARD_NUM * CARD_MARK_NUM)  // トランプに含まれるカードの枚数

オブジェクト形式マクロの用途は主に2つあります。

  1. コンパイルさせるコードを切り替える
  2. リテラルに名前を付ける

1番の用途は、「ヘッダファイル」のページで説明したインクルードガードなどが当てはまります。この用途では、後述する #ifdef などの分岐命令を併用します。

2番の用途は constexpr変数で代替できますし、そうするべきです。オブジェクト形式マクロには型の指定がないため、コンパイラのチェックの恩恵が受けづらくなります。

#define HAND_CARD_NUM (5)          // 避ける
constexpr auto hand_card_num = 5;  // 推奨

ただし、後述する #if の条件式で使う定数に名前を与える目的のときには #define を使わなければなりません。

オブジェクト形式マクロの「置換後の文字の並び」を () で取り囲むと安全性が増す場合があります。たとえば、3 + 2 のようなコードに置換される場合、() で囲っていないと、使い方によっては意図しない結果になります。

#include <iostream>

#define BAD_MACRO     3 + 2
#define BETTER_MACRO  (3 + 2)

int main()
{
    std::cout << BAD_MACRO * 10 << "\n";
    std::cout << BETTER_MACRO * 10 << "\n";
}

実行結果:

23
50

3 + 2 は、つまり 5 ですから、10倍したら 50 になることが望まれる結果です。「置換後の文字の並び」を () で囲っている BETTER_MACRO のほうは正しい結果になりますが、BAD_MACRO の方は間違った結果になっています。こうなってしまうのは、プリプロセスを終えたとき、ソースコードがどうなるかを考えればわかります。

int main()
{
    std::cout << 3 + 2 * 10 << "\n";
    std::cout << (3 + 2) * 10 << "\n";
}

計算の順序の規則により、加算よりも乗算が優先されるため、3 + 2 * 10 は想定した結果にならないわけです。

関数形式マクロ

関数形式マクロは、引数があるマクロで、関数に似ているためこう呼ばれます。「置換後の文字の並び」の中で、渡されてきた実引数を使用できます。

本物の関数と違い、仮引数は名前があるだけで、型は指定しません。仮引数が不要なら () の内側を空白にします。

#define PRINT_VALUE(x)  std::cout << x << "\n"
#define PRINT_NAME_AND_VALUE(name, value)  std::cout << name << ": " << value << "\n"
#define PRINT_EMPTY()  std::cout << "\n"

一般的に、関数形式マクロの「置換後の文字の並び」の行末に ; は不要です。マクロを使う側が ; を書くほうが自然ですし、むしろ定義側に ; があると邪魔になることがあります。

関数形式マクロを使用する側は、関数と同じように () を使います。仮引数があるのなら、対応する実引数も指定します。

PRINT_VALUE(123);
PRINT_NAME_AND_VALUE("xyz", 400);
PRINT_EMPTY();

【上級】仮引数の個数を自由にできる可変引数マクロも実現できます。この話題はページを改めて取り上げることにします。

関数形式マクロの用途には、以下のようなものがあります。

  1. 関数呼出しのコストを避ける
  2. 型を問わない関数を作る

ただし、このあと説明するように、関数形式マクロには、完全に解決することができない問題があるため、原則として使用を避けるべきです。代替策として、関数テンプレート (function template) やインライン関数 (inline function)がありますが、ここではこれらの代替策についての話は省き、関数形式マクロで起こる問題の説明だけを行うことにします。


1つ目の用途は、マクロの処理がプリプロセス時点で終了することを活かして、プログラムの実行時の関数呼出しをなくして、高速化を図るというものです。たとえば、ループの内側で呼び出される関数は、そのループの回数分だけ、関数呼出しのコストが掛かることになりますが、関数形式マクロにすれば、関数呼出しは1回たりも行わずに済みます。

2つ目の用途は、まったく同じコードで実装できる関数だが、仮引数や戻り値の型が異なるため、別の関数にしなければならない場合に対応することです。たとえば、仮引数 a と b に対して return a + b; とするだけの場合、a と b は int型でも long long型でも double型でも同じコードになります。関数形式マクロには型の指定がないため、1つのマクロに集約できます。

#include <iostream>

#define ADD(a, b)  ((a) + (b))

int main()
{
    std::cout << ADD(3, 5) << "\n";
    std::cout << ADD(123LL, -340LL) << "\n";
    std::cout << ADD(2.34, 6.15) << "\n";
}

実行結果:

8
-217
8.49

「置換後の文字の並び」の中に現れる引数は、1つ1つ () で囲むようにすると安全性が増します。全体を () で囲むことも多くの場合は有効ですが、置換内容によっては余計になるかもしれません。

#include <iostream>

#define BAD_MUL(a, b)  (a * b)
#define BETTER_MUL(a, b)  ((a) * (b))

int main()
{
    std::cout << BAD_MUL(5 + 2, 5) << "\n";
    std::cout << BETTER_MUL(5 + 2, 5) << "\n";
}

実行結果:

15
35

5 + 2 つまり 7 を 5倍しているつもりなので、35 が望まれる結果です。BAD_MUL の方の置換後の文字の並びは、5 + 2 * 5 となってしまうため、15 という間違った結果になりました。

() で丁寧に囲ったとしても、インクリメントやデクリメントを使われると問題になることがあります。そして、この問題は、使う側が気を付ける以外に解決策がありません。2つの引数のうち、大きい方に置換される MAX マクロで考えてみます。

#include <iostream>

#define MAX(a,b) ((a) > (b) ? (a) : (b))

int main()
{
    int a {10};
    std::cout << MAX(a, 5) << "\n";
    std::cout << MAX(a + 1, 5) << "\n";
    std::cout << MAX(++a, 5) << "\n";
}

実行結果:

10
11
12

最初の2つの使い方には問題ありませんが、3つ目の結果は意外なものです。10 が入った変数 a をインクリメントしているので、想定される結果は 11 ですが、実際には 12 が出力されています。こうなってしまうのは、置換後の文字の並びが ((++a) > (5) ? (++a) : (5)) となるからです。条件式のところで1度インクリメントされたあと、結果の値のところでも再びインクリメントされています。

#演算子

置換後の文字の並びの中で、マクロの仮引数の名前の頭に # を付加すると、実引数として渡された内容を文字列にしたものへ置き換えられます。この # は、関数形式マクロでのみ使用できる演算子の一種です。

次のプログラムでは、変数の値を、その変数の名前とともに出力する関数形式マクロを定義しています。

#include <iostream>

#define PRINT(v)    std::cout << #v << " = " << (v) << "\n"

int main()
{
    int x {123};
    double y {3.14};

    PRINT(x);
    PRINT(y);
}

実行結果:

x = 123
y = 3.14

##演算子

置換後の文字の並びの中で ## を使うと、2つの文字の並びを連結できます。この ## は、マクロでのみ使用できる演算子の一種です。

#include <iostream>

#define VALUE(number)   value ## number

int main()
{
    int value1 {10};
    int value2 {20};
    int value3 {30};

    std::cout << VALUE(1) << "\n";
    std::cout << VALUE(2) << "\n";
    std::cout << VALUE(3) << "\n";
}

実行結果:

10
20
30

VALUE マクロは、value という文字の並びと、仮引数number に渡されてきた文字の並びを連結しています。そのため、VALUE(1)value ## 1 を意味しており、value1 に置換されます。

マクロの置換はプリプロセスで行われることなので、次のようには書けません。

#include <iostream>

#define VALUE(number)   value ## number

int main()
{
    int value1 {10};
    int value2 {20};
    int value3 {30};

    for (int i = 1; i <= 3; ++i) {
        std::cout << VALUE(i) << "\n";
    }
}

VALUE(i)value ## i であり、置換結果は valuei です。プリプロセスの時点では i は i でしかありません。これは求めていた結果ではないばかりか、未定義の動作ですらあります。##演算子によって作られる文字の並びは、前処理トークン (preprocessing token) と呼ばれる、意味が特定できる文字の並びでなければ未定義とされているためです4

前処理トークンは、識別子や各種リテラル、ヘッダファイルの名前といったものです。5

なお、#演算子と ##演算子が両方登場する場合、どちらが先に評価されるかは未規定です6

事前定義マクロ

自動的に定義済みになっているマクロがいくつか存在しています。このようなマクロは、事前定義マクロ (predefined macro) と呼ばれ、いつでも使用できます。

代表的な事前定義マクロとして、以下のものがあります。

完全な一覧は規格文書を参照してください。7

名前 置換結果
__FILE__ これを記述したソースファイルの名前(文字列リテラル)
__LINE__ これを記述したソースファイル上での行番号(整数リテラル)
__DATE__ これを記述したソースファイルがプリプロセスで処理されたときの日付(文字列リテラルで “Mmm dd yyyy” 形式)
__TIME__ これを記述したソースファイルがプリプロセスで処理されたときの時刻(文字列リテラルで “hh:mm:ss” 形式)

__FILE__ と __LINE__ の置換結果は、このあと説明する #line の影響を受けます。

【上級】__DATE__ の置換結果のうち、Mmm(月)にあたる部分は、std::asctime関数によって生成される月の表現と同じものが使われます。dd(日)は必ず2桁分の幅を取り、10未満のときは上位桁が空白文字になります。8

【上級】__TIME__ の置換結果は、std::asctime関数によって生成される表現と同じものが使われます。9

【C++20】std::source_locationクラスが追加され、ソースファイルの名前、行番号、列番号、関数名をまとめて取得、保持できるようになりました10

#include <iostream>

int main()
{
    std::cout << __FILE__ << "\n";
    std::cout << __LINE__ << "\n";
    std::cout << __DATE__ << "\n";
    std::cout << __TIME__ << "\n";
}

実行結果:

c:\test_program\main.cpp
6
Feb  5 2022
13:22:25

プリプロセスの機能なので、実行するたびに結果が変わるというものではありません。

【上級】__DATE__ と __TIME__ を使っていると、同じソースファイルをコンパイルしても、異なるオブジェクトファイルが生成されることに注意してください。このため、実行可能ファイルも異なるものになります。たとえば複数のプログラマーで開発を行っている場合、ほかの作業者がビルドした結果とは異なるものが生成されることになります。

__func__

さきほど挙げた事前定義マクロと非常によく似ているものに、__func__ があります。こちらは正確には変数(事前定義変数 (predefined variable))であり11、プリプロセスで処理されているわけではないですが、その違いを気にする必要はほとんどありません。

__func__ は、関数の本体でのみ使用でき、その関数の名前の文字列表現になります。

#include <iostream>

void my_function()
{
    std::cout << __func__ << "\n";
}

int main()
{
    std::cout << __func__ << "\n";
    my_function();

    struct S {
        void operator()() {
            std::cout << __func__ << "\n";
        }
    };
    S s {};
    s();
}

実行結果:

main
my_function
operator ()

__func__ は、各々の関数定義の内側で、暗黙的に定義される変数です。その値は関数名を表現した文字列ではありますが、具体的な表現方法は実装に任されています。

【上級】static const char __func__[] = "関数名"; のように定義されます。

#line

#line は、ソースファイルの行番号や名前を制御します。

#line の使い方は次の2通りあります。

#line 行番号
#line 行番号 ソースファイル名

いずれにしても、#line を記述した行の行番号を「行番号」に指定した数とみなすようにプリプロセッサに指示を与えます。「ソースファイル名」を指定した場合は、同様に #line を記述した行以降のソースファイル名を「ソースファイル名」とみなすようになります。その結果、__FILE__ や __LINE__ の置換結果が影響を受けます。また、処理系がエラーや警告などを報告するときに表示されるソースファイル名や行番号にも影響することがあります。

#include <iostream>

int main()
{
    std::cout << __FILE__ << "\n";
    std::cout << __LINE__ << "\n";

#line 1000 "new_name.cpp"
    std::cout << __FILE__ << "\n";
    std::cout << __LINE__ << "\n";
}

実行結果:

c:\test_program\main.cpp
6
c:\test_program\new_name.cpp
1001

分岐 (#if、#elif、#else、#endif)

プリプロセスの中で分岐構造を作る方法があり、条件によって、コンパイルする部分としない部分を作れます。このようにコンパイルを制御することを、条件コンパイル (conditional compilation) と呼びます。

もっとも単純な分岐構造は、#if#endif によって構築できます。

#if 条件式
    条件式が真のときにコンパイルされるコード
#endif

「条件式」の結果が 0 以外になる場合に、「条件式が真のときにコンパイルされるコード」の部分がコンパイルされます。プリプロセスの時点で評価できなければならないため定数式でなければなりません。

通常の if文に対する else は #else で、else if は #elif で表現します。

#if 条件式1
    条件式1が真のときにコンパイルされるコード
#elif 条件式2
    条件式1が偽、条件式2が真のときにコンパイルされるコード
#else
    条件式1、条件式2がともに偽のときにコンパイルされるコード
#endif

以下は使用例です。PROGRAM_MODE の置換後の値を変更してビルドしなおすと、有効になるコードの部分が変化します。

#include <iostream>

#define PROGRAM_MODE    (1)

int main()
{
#if PROGRAM_MODE == 0
    std::cout << "program mode 0.\n";
#elif PROGRAM_MODE == 1
    std::cout << "program mode 1.\n";
#else
    std::cout << "program mode is unknown.\n";
#endif
}

実行結果:

program mode 1.

なお、PROGRAM_MODE を constexpr変数に変えることは不適切であることに注意してください。

#include <iostream>

constexpr auto program_mode = 1;

int main()
{
#if program_mode == 0
    std::cout << "program mode 0.\n";
#elif program_mode == 1
    std::cout << "program mode 1.\n";
#else
    std::cout << "program mode is unknown.\n";
#endif
}

実行結果:

program mode 0.

program_mode の値は 1 ですが、実行結果をみると 0 のときのコードが有効になっていることが分かります。このような結果になるのは、#if の条件式の中で、マクロ名以外の識別子やキーワードは 0 で置き換えられる(true と false だけは例外でそのまま扱われる)というルールがあるためです12そのため、constexpr変数の名前(=識別子)である program_mode0 に置き換わっており、#if program_mode == 0 のところが真となります。


なお、有効にならない部分のコードはコンパイルされないため、プリプロセスでの分岐構造を、コメントアウトの代わりに用いることがあります(#if 0 ~ #endif で囲む)。/* ~ */ と違って、ネストできる利点があるほか、01 に変えるだけでアンコメントできる手軽さもあります。

#error

さきほどのサンプルプログラムで、PROGRAM_MODE の値が 0 でも 1 でもないときは、モード不明としてエラーにしたいかもしれません。そのようなときには、#error を使います。#error は強制的にエラーを発生させ、プリプロセスを終了させます。

#error の使い方は次のとおりです。

#error エラーメッセージ

#error の行が有効になった場合、エラーになり、「エラーメッセージ」の内容が出力されます。

さきほどのサンプルプログラムを書き換えてみます。

#include <iostream>

#define PROGRAM_MODE    (2)

int main()
{
#if PROGRAM_MODE == 0
    std::cout << "program mode 0.\n";
#elif PROGRAM_MODE == 1
    std::cout << "program mode 1.\n";
#else
    #error "program mode is unknown.\n";
#endif
}

Visual Studio 2015 では、次のエラーが報告されます。

1>  main.cpp
1>c:\test_program\main.cpp(12): fatal error C1189: #error:  "program mode is unknown.\n";

マクロによる分岐 (#ifdef、#ifndef、defined)

プリプロセスによる分岐にはもう1種類あって、マクロが定義されているかどうかを判断基準にします。

#ifdef はマクロが定義されているかどうか、#ifndef はマクロが定義されていないかどうかを判定します。いずれも #else を加えることが可能です。

#ifdef マクロ名
    マクロが定義されているときにコンパイルされるコード
#endif

#ifndef マクロ名
    マクロが定義されていないときにコンパイルされるコード
#endif

#undef によって定義が消されている場合、そこから後ろの位置では「定義されていない」とみなされます。

#if と defined を組み合わせて実現することもできます。defined は、指定したマクロが定義されていたら 1 に、定義されていなければ 0 になります。

#if defined(マクロ名)
    マクロが定義されているときにコンパイルされるコード
#endif

// ( ) はなくても同じ
#if defined マクロ名
    マクロが定義されているときにコンパイルされるコード
#endif

こうした分岐の制御のためだけに使うマクロは、置換後の結果には興味がないので、「置換後の文字の並び」を省略して定義することもあります。

#define DEBUG_MODE
#ifdef DEBUG_MODE
// ...
#endif

次のサンプルは、標準入力から5つの整数を受け取り、その合計を出力するだけの簡単なプログラムです。NEGATIVE_NUMBER_NOT_ALLOW マクロが定義されているときには、負数をエラーとして扱うようにします。

#include <iostream>

#define NEGATIVE_NUMBER_NOT_ALLOW   // 有効な場合、負数を許可しない

int main()
{
    int total_values {0};

    for (int i = 0; i < 5; ++i) {

#ifdef NEGATIVE_NUMBER_NOT_ALLOW
        std::cout << "Please enter the integer greater than or equal to 0.\n";
#else
        std::cout << "Please enter the integer.\n";
#endif

        int value {};
        std::cin >> value;

#ifdef NEGATIVE_NUMBER_NOT_ALLOW
        if (value < 0) {
            std::cout << "error.\n";
            return 1;
        }
#endif

        total_values += value;
    }

    std::cout << "total: " << total_values << "\n";
}

実行結果:

program mode 0.

プラグマ

最後にプラグマ (pragma) を紹介します。プラグマは、処理系が独自に実装している、標準でない機能を使うための構文で、次のように #pragma を使って記述します。

#pragma *****

***** の部分は、使おうとする機能によって異なります。標準のものではないため、処理系のドキュメントを確認するなどして理解しなければなりません。

【上級】解説は省きますが、#pramga を #define の「置換後の文字の並び」のところで使えない問題を解決する方法として、_Pragma演算子があります13

比較的よく認知されているプラグマとして、#pragma once があります。これはインクルードガード(「ヘッダファイル」のページを参照)を簡単に実現します。ヘッダファイルの先頭に #pragma once と記述するだけで、インクルードガードの役割を果たします。

#pragma once

// ほかのコード

主要なコンパイラではインクルードガードとして機能しますが、あくまでも処理系定義の機能です。これを認識しないコンパイラでは、単にこの記述を無視するかもしれません。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

ある関数の中でだけ有効になるマクロをどのように実現できますか?

解答・解説

問題2 (基本★★)

ソースコード上のある地点で、ソースファイルの名前と行番号、実行中の関数の名前を出力するデバッグ目的の関数を次のように作成しました。しかし、この関数には実用上の問題があります。問題点を指摘してください。また、どうすれば解決できるでしょうか?

void print_source_location()
{
    std::cout << "File: " << __FILE__ << "  Line: " << __LINE__ << "  Func: " << __func__ << "\n";
}

解答・解説

問題3 (応用★★)

2つの変数の値を交換する関数形式マクロを作成してください。

解答・解説

問題4 (応用★★★)

ポーカーのプログラムで、カードの番号とマークを出力するとき、現在は club A とか diamond 2 のような表記になっています。マクロによる分岐を使って、クラブのAダイヤの2 といった表記で出力するコードに切り替えられるようにしてください。

現在のポーカープログラムの最終形は、「ヘッダファイル」のページの練習問題の解答にあります。

解答・解説

問題5 (調査★★★)

使っているコンパイラが実装しているプラグマについて調べてみてください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
Twitter でツイート Twitter をフォロー LINE で送る
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー