読者です 読者をやめる 読者になる 読者になる

Flat Leon Works

アプリやゲームを作ってます。

【C++】プリプロセッサの基礎(マクロ編)

マクロとは

C++におけるマクロとは、ソースコードコンパイル前のプリプロセス処理で行われるテキストの置換処理のことです。プリプロセス処理にはマクロの他に#include#ifなどがあります。

マクロの役割

マクロの利用場面は以下とおりだと思います。

  • マクロにのみ用意された機能を使う
  • C++文法では実現できないことを行う
  • コードの重複を除去する
  • 意味的に本質的でないコードの隠蔽

マクロは言語に拡張性を持たせる素晴らしい機能だと思います。うまく活用するとコーディングの手間を大幅に減らし、コードの可読性も上げることができます。

マクロを定義する

マクロは#defineによって定義することができます。引数を受け取らない定数マクロと引数を受け取る関数マクロの2種類があります。

  • 定数マクロ : #define マクロ名 置換テキスト
  • 関数マクロ : #define マクロ名( マクロ引数 ) 置換テキスト

定数マクロはそのまま置換されます。関数マクロは引数を受け取ることができるので柔軟な置換を行うことができます。

定数マクロの例

#define BOX_HEIGHT 30

定数マクロはソースコード中で使う定数にわかりやすいように名前を付けるために使うのが基本的な用途です。定数マクロなどを使わずにソースコードに直接現れる数値をマジックナンバーと呼び、避けるべきとされています。定数マクロではなくstatic const 変数を使うべきとの意見もありますが、定数マクロの方が楽で柔軟性も高いので個人的には定数マクロの方を多用しています。

#define USE_DEBUG 0

void func( int value )
{
    #if USE_DEBUG
    printf( "Value:%d", value );
    #endif
    〜 処理 〜
}

定数マクロのもう一つの使い方は、#ifによる動作の切り替えです。#if#endifで囲われた部分は#ifの条件式が偽の場合、取り除かれてからコンパイラに渡されます。これはstatic const変数では代用できない強力な機能です。

関数マクロの例

    #include <stdio.h>
     
    #define MY_LOG( log ) printf( "File:%s Line:%d Func:%s Log:%s\n", __FILE__, __LINE__, __func__, log )
     
    int main() {
        MY_LOG( "test" ); // 出力:File:Prog.cpp Line:6 Func:main Log:test
        return 0;
    }

関数マクロは引数を受け取るので、定数マクロより汎用性が高くいろいろな場面で使うことができます。__FILE__などの意味は後述の組み込みマクロの項目を見てください。

定義したマクロを削除する

#undefを使うと定義済みのマクロの定義を削除することができます。定義が削除されたマクロは再び定義することができるようになります。

#define BOX_COUNT 7 // マクロ定義

#undef BOX_COUNT // マクロ定義削除

#define BOX_COUNT 8 // またマクロ定義

#ifでマクロが定義されているかどうかを利用する

#ifdef AAA
// AAA が定義されている場合
#endif

#ifndef AAA
// AAA が定義されていない場合
#end

#if defined( AAA ) // #ifdefと同じ
// AAA が定義されている場合
#endif

#if !defined( AAA ) // #ifndefと同じ
// AAA が定義されていない場合
#endif

#ifdef BBB
// BBBが定義されている場合
#elif defined( AAA )
// BBBが定義されていなくて、AAA が定義されている場合
#endif

#ifdef, #ifndef, defined()を使えば、マクロが定義されているかどうかで処理を分けることができます。

組み込みマクロ

最初から用意されている特殊な定数マクロがいくつか存在します。

  • __FILE__ : 展開された場所のファイル名文字列に置換されます
  • __LINE__ : 展開された場所の行番号数値に置換されます
  • __func__ : 展開された場所の関数名文字列に置換されます
  • __VA_ARGS__ : 関数マクロで受け取った可変個引数

__FILE__, __LINE__, __func__はログやアサートなどに使うと便利です。他にもいくつかあるようですが使ったことがないです。

他の定義済みマクロはこの辺を参考にしてください。

可変個引数のマクロ

#include <stdio.h>
#define PRINT( ... ) printf( __VA_ARGS__ )
int main() {
    PRINT( "%s %d", "a", 99  );
    return 0;
}

マクロ引数を...にすると可変個の引数を受け取る関数マクロを定義できます。受け取った引数は__VA_ARGS__で使うことができます。

マクロ引数の文字列定数化

#include <stdio.h>
#define TO_STR( param ) #param
#define TO_STR_VA( ... ) #__VA_ARGS__
int main() {
    printf( "%s\n", TO_STR( aaa )  ); // 出力:aaa
    printf( "%s\n", TO_STR_VA( aaa, bbb, ccc )  ); // 出力:aaa, bbb, ccc
    return 0;
}

受け取ったマクロ引数の頭に#をつけると"文字列定数化"することができます。__VA_ARGS__にも使うことができるようです。

マクロ引数の連結

#include <stdio.h>
#define JOIN( x, y ) x ## y
int main() {
    const char* apple = "ringo";
    const char* orange = "mikan";
    printf( "%s, %s", JOIN( app, le ), JOIN( o, range )  ); // 出力:ringo, mikan
    return 0;
}

受け取ったマクロ引数は##で連結することができます。

複数行に渡るマクロ定義を行う

#define ADD( x, y ) (\
(x) + (y) \
)

'\'を行末につけると改行が無視されます。それを利用してマクロの定義を複数行で行うことができます。

マクロ引数のマクロが展開されるようにする

#include <stdio.h>
#define TO_STR( param ) #param
#define TO_STR2( param ) TO_STR2_( param )
#define TO_STR2_( param ) #param
#define COUNT 10
int main() {
    printf( "%s\n", TO_STR( COUNT ) ); // 出力:COUNT
    printf( "%s\n", TO_STR2( COUNT ) );// 出力:10
    return 0;
}

マクロ引数がマクロだった場合、そのマクロ引数の展開は#による文字列化や##による連結の後に行われてしまうようです。これを回避するには、関数マクロの実装を2段階にします。最初の関数マクロは受け取った引数をそのまま2段階目の関数マクロになげ、2段階目の関数マクロが実際の処理を行います。(マクロ関数TO_STR2を参照)

マクロの罠

演算子の優先順序

#include <stdio.h>
#define MULTI( x, y ) x * y
#define MULTI2( x, y ) ((x) * (y))

int main() {
    printf( "%d\n", MULTI( 10 + 5,  3 ) ); // 15 * 3 -> 45 のつもりが、10 + 5 * 3 -> 25 になってしまう
    printf( "%d\n", MULTI2( 10 + 5,  3 ) ); // 15 * 3 -> 45 になる
    return 0;
}

関数マクロはテキストの置換なので、マクロ引数をそのまま使うと演算子の優先順序が意図しないものになってしまう場合があります。これを回避するのは簡単です。マクロ引数で演算を行う場合は、個々のマクロ引数を()でくくり、全体も()でくくります。ただし、すべての関数マクロでこれを行う必要はありません。演算子の優先順位が問題になる場合のみ行います。

意図しない名前の上書き

マクロの定義時、すでに定義済みの同名のマクロがある場合はプリプロセス処理の段階でエラーになりますが、変数や関数と同じ名前を定義してもプリプロセス段階ではエラーになりません。たいていの場合はコンパイル段階でエラーになるのですが、偶然にも文法的に正しく、コンパイルが通ってしまう場合もあります。その場合意図しない挙動となってしまいます。

#include <stdio.h>

// #include "A.h"
    struct A
    {
        int GetMax() const { return 7; }
    };
 
// #include "hoge.h"
    〜 長いコード 〜
    #define GetMax() 20
    〜 長いコード 〜
 
struct B : public A
{
    int GetBaseMax() const { return GetMax(); }
};
 
int main() {
    B b;
    printf( "%d\n", b.GetBaseMax() ); // 20
    return 0;
}

上記例では、基底クラスのGetMaxを呼び出しているつもりが、いつのまにか関数マクロで置き換わっています。そしてコンパイルも通ってしまっています。

このような事態を避けるためには以下のことを意識するといいと思います。

  • マクロを不必要に広い範囲から使えるようにしない
  • 不要になったら#undefで定義を消す
  • マクロ名はすべて大文字にする
  • マクロ名にプリフィックスを付ける
  • 短いマクロ名は避ける

無駄なマクロの使用を避ける

マクロは便利なものですが、マクロを使わなくても済む場合はそちらを使えば当然これらのマクロの罠は避けられます。

  • 関数マクロ : template関数
  • 定数マクロ : enum, static const変数, typedef

まとめ

以上がマクロの基礎に関する解説になります。マクロはC++の大きな魅力の一つだと思います。実際にマクロを活用した例もそのうち紹介したいと思います。