C++のマクロは、コンパイル前の前処理段階でコードを展開する仕組みです。
条件付きコンパイルやOS・コンパイラごとの処理切り替えなど、通常の関数や変数では対応しにくい場面で活用されています。
一方で、マクロには型やスコープがなく、使い方によってはコードの可読性や保守性を下げることがあります。
現代のC++では、定数や単純な処理にはconstexpr、関数、テンプレートなどを使い、マクロは必要な用途に限定するのが基本です。
この記事では、C++マクロの仕組みや代表的な使い方、安全に利用するためのポイントを詳しく解説します。
C++マクロとは
C++のマクロは、プリプロセッサによってコンパイル前に処理されます。
代表的な定義方法は#defineです。
#define BUFFER_SIZE 1024
ソースコード内でBUFFER_SIZEが使われると、プリプロセッサによって1024へ展開されます。
char buffer[BUFFER_SIZE];
展開後は、概念的に次のようなコードとしてコンパイラへ渡されます。
char buffer[1024];
マクロは前処理トークンを展開する仕組み
マクロは文字列置換として説明されることがありますが、厳密には前処理トークン単位で展開されます。
そのため、C++の変数や関数とは異なり、型チェックや名前空間による管理は行われません。
マクロを利用する際は、通常のC++コードとは異なる仕組みで動作していることを理解しておく必要があります。
定数マクロよりconstexprを使う
古いCやC++のコードでは、定数をマクロで定義する方法がよく使われています。
#define MAX_COUNT 100
現代のC++では、constexprを使う方が安全です。
inline constexpr int MaxCount = 100;
constexprを使うメリット
constexprには型があるため、コンパイラによる型チェックを受けられます。
また、名前空間やクラスの中に定義できます。
namespace config {
inline constexpr int MaxCount = 100;
}
使用時も、どこに属する定数なのかが明確です。
int count = config::MaxCount;
マクロには名前空間がないため、別のヘッダーファイルやライブラリで同じ名前が使われると衝突する可能性があります。
文字列定数にはstd::string_viewも便利
文字列定数は、次のように定義できます。
inline constexpr const char* AppName = "SampleApp";
文字列として扱うことを明確にしたい場合は、std::string_viewも有効です。
#include <string_view>
inline constexpr std::string_view AppName = "SampleApp";
比較や長さの取得を行う場合にも使いやすくなります。
関数形式マクロの注意点
引数を受け取る関数形式マクロは、簡単な処理を短く書ける一方で、演算子の優先順位や引数の評価回数に注意が必要です。
引数と式全体を括弧で囲む
次のマクロを考えます。
#define SQUARE(x) x * x
このマクロを次のように使用します。
int result = SQUARE(2 + 3);
展開後は次のようになります。
int result = 2 + 3 * 2 + 3;
演算子の優先順位によって、想定した結果になりません。
関数形式マクロでは、引数と式全体を括弧で囲みます。
#define SQUARE(x) ((x) * (x))
これにより、次のコードは意図した形に展開されます。
int result = SQUARE(2 + 3);
int result = ((2 + 3) * (2 + 3));
引数の多重評価に注意する
括弧を付けても、引数が複数回展開される問題は残ります。
#define SQUARE(x) ((x) * (x))
次のように副作用を持つ式を渡すと危険です。
int value = 3;
int result = SQUARE(value++);
展開後は次のようになります。
int result = ((value++) * (value++));
同じ変数が複数回変更されるため、未定義動作につながる可能性があります。
このような処理には、通常の関数やテンプレートを使います。
template <typename T>
constexpr auto square(T value)
{
return value * value;
}
使用例です。
int value = 3;
int result = square(value++);
関数呼び出しでは、value++は一度だけ評価されます。
条件付きコンパイルにマクロを使う
マクロが特に有効なのが、条件付きコンパイルです。
特定のビルド設定や環境に応じて、コンパイル対象のコードを切り替えられます。
#ifdef DEBUG
std::cout << "Debug mode\n";
#endif
DEBUGが定義されている場合だけ、該当コードがコンパイルされます。
#ifdefと#ifndef
#ifdefは、指定したマクロが定義されているかを確認します。
#ifdef ENABLE_LOG
// ログ機能を有効化
#endif
#ifndefは、指定したマクロが定義されていない場合に処理します。
#ifndef ENABLE_LOG
// ログ機能が無効な場合の処理
#endif
#ifと#elif
値付きマクロを使う場合は、#ifや#elifで分岐できます。
#define LOG_LEVEL 2
#if LOG_LEVEL == 0
// ログを出力しない
#elif LOG_LEVEL == 1
// エラーログのみ
#elif LOG_LEVEL == 2
// 詳細ログを出力
#else
// その他の設定
#endif
definedで複数条件を組み合わせる
definedを使うと、複数のマクロ条件を組み合わせられます。
#if defined(DEBUG) && defined(ENABLE_LOG)
std::cout << "Debug logging enabled\n";
#endif
OSごとに処理を切り替える
Windows、macOS、Linuxなど、OSごとに処理を切り替える場合にもマクロが使われます。
#if defined(_WIN32)
std::cout << "Windows\n";
#elif defined(__APPLE__)
std::cout << "macOS\n";
#elif defined(__linux__)
std::cout << "Linux\n";
#else
std::cout << "Unknown platform\n";
#endif
OS固有のヘッダーを切り替える
OSごとに異なるヘッダーファイルを読み込むこともできます。
#if defined(_WIN32)
#include <windows.h>
#elif defined(__linux__)
#include <unistd.h>
#endif
ただし、条件分岐が増えすぎるとコードが読みにくくなります。
規模の大きいプロジェクトでは、OSごとに実装ファイルを分け、ビルドシステム側で対象ファイルを選択する方法も有効です。
platform/
├── platform_windows.cpp
├── platform_linux.cpp
├── platform_macos.cpp
└── platform.hpp
定義済みマクロをログに活用する
C++には、コンパイラによって自動的に定義されるマクロがあります。
代表的なものは次のとおりです。
| マクロ | 内容 |
|---|---|
__FILE__ | 現在のファイル名 |
__LINE__ | 現在の行番号 |
__DATE__ | コンパイル日 |
__TIME__ | コンパイル時刻 |
__cplusplus | C++標準のバージョンを示す値 |
ファイル名と行番号を出力する
ログマクロに__FILE__と__LINE__を組み合わせると、エラーが発生した場所を表示できます。
#define LOG(message) \
std::cerr << "[" << __FILE__ << ":" << __LINE__ << "] " \
<< message << '\n'
使用例です。
LOG("Connection failed");
出力例です。
[main.cpp:42] Connection failed
C++20以降はstd::source_locationも使える
C++20以降では、std::source_locationを使って呼び出し位置を取得できます。
#include <iostream>
#include <source_location>
#include <string_view>
void log_error(
std::string_view message,
std::source_location location =
std::source_location::current())
{
std::cerr
<< "[" << location.file_name()
<< ":" << location.line()
<< "] ERROR: "
<< message
<< '\n';
}
使用側は通常の関数呼び出しです。
log_error("Connection failed");
マクロを使わずに、ファイル名や行番号を取得できます。
複数文マクロにはdo-whileを使う
複数の文を含むマクロは、そのまま定義するとif文などと組み合わせた際に問題が起こることがあります。
#define LOG_ERROR(message) \
std::cerr << "ERROR: "; \
std::cerr << message << '\n';
次のように使用すると、構文が崩れる可能性があります。
if (failed)
LOG_ERROR("Operation failed");
else
recover();
do-while(false)で1つの文として扱う
複数文マクロは、do-while(false)で囲むのが一般的です。
#define LOG_ERROR(message) \
do { \
std::cerr << "ERROR: "; \
std::cerr << message << '\n'; \
} while (false)
使用側では、通常の文と同じようにセミコロンを付けます。
if (failed)
LOG_ERROR("Operation failed");
else
recover();
これにより、マクロ全体が1つの文として扱われます。
可変長引数マクロを使う
マクロは、可変長引数を受け取ることもできます。
#define PRINTF_LOG(format, ...) \
std::printf(format, __VA_ARGS__)
使用例です。
PRINTF_LOG("value = %d\n", 10);
C++20では__VA_OPT__を利用できる
可変長引数が空の場合、余分なカンマが残ることがあります。
C++20以降では、__VA_OPT__を利用できます。
#define PRINTF_LOG(format, ...) \
std::printf(format __VA_OPT__(,) __VA_ARGS__)
引数がない場合は、カンマが挿入されません。
PRINTF_LOG("finished\n");
引数がある場合は、通常どおり展開されます。
PRINTF_LOG("value = %d\n", 10);
文字列リテラルを前提とするログマクロ
次のように文字列を連結するログマクロでは、第1引数に文字列リテラルを渡す必要があります。
#define PROJECT_PRINTF_LOG(format_literal, ...) \
do { \
std::fprintf( \
stderr, \
"[DEBUG] %s:%d: " format_literal "\n", \
__FILE__, \
__LINE__ \
__VA_OPT__(,) __VA_ARGS__ \
); \
} while (false)
使用例です。
PROJECT_PRINTF_LOG("User ID: %d", user_id);
実行時に作成された文字列変数を渡す用途には向きません。
文字列化演算子#
マクロ引数の前に#を付けると、その引数を文字列へ変換できます。
#define STRINGIFY(x) #x
使用例です。
std::cout << STRINGIFY(hello) << '\n';
出力は次のようになります。
hello
式と値を同時に表示する
文字列化を使うと、式そのものと評価結果を同時に表示できます。
#define PRINT_VALUE(expression) \
std::cout << #expression << " = " << (expression) << '\n'
使用例です。
int count = 10;
PRINT_VALUE(count);
PRINT_VALUE(count * 2);
出力例です。
count = 10
count * 2 = 20
トークン連結演算子##
##を使うと、複数のトークンを連結できます。
#define MAKE_NAME(prefix, number) prefix##number
使用例です。
int value1 = 100;
std::cout << MAKE_NAME(value, 1) << '\n';
展開後は次のようになります。
std::cout << value1 << '\n';
変数名を自動生成する
#define DECLARE_VARIABLE(number) \
int variable_##number = number
使用例です。
DECLARE_VARIABLE(1);
DECLARE_VARIABLE(2);
DECLARE_VARIABLE(3);
展開後は次のようになります。
int variable_1 = 1;
int variable_2 = 2;
int variable_3 = 3;
ただし、単純なデータの集合を扱う場合は、配列やコンテナの方が読みやすくなることがあります。
std::array<int, 3> values{1, 2, 3};
マクロの二段階展開
文字列化やトークン連結では、マクロ引数の展開順序に注意が必要です。
#define VERSION 123
#define STRINGIFY(x) #x
const char* text = STRINGIFY(VERSION);
この場合、結果は"123"ではなく"VERSION"になります。
マクロの値を展開してから文字列化するには、二段階に分けます。
#define STRINGIFY_IMPL(x) #x
#define STRINGIFY(x) STRINGIFY_IMPL(x)
使用例です。
#define VERSION 123
const char* text = STRINGIFY(VERSION);
結果は次のようになります。
const char* text = "123";
トークン連結でも同じように二段階展開を使います。
#define CONCAT_IMPL(a, b) a##b
#define CONCAT(a, b) CONCAT_IMPL(a, b)
一意な識別子を生成する
__LINE__を使うと、呼び出した行番号を含む識別子を生成できます。
#define CONCAT_IMPL(a, b) a##b
#define CONCAT(a, b) CONCAT_IMPL(a, b)
#define UNIQUE_NAME(prefix) CONCAT(prefix, __LINE__)
使用例です。
int UNIQUE_NAME(temp_) = 10;
int UNIQUE_NAME(temp_) = 20;
異なる行で使用すれば、異なる変数名になります。
__COUNTER__を使う方法
主要なコンパイラでは、__COUNTER__を使える場合があります。
#define UNIQUE_NAME(prefix) CONCAT(prefix, __COUNTER__)
展開されるたびに値が増えるため、識別子の重複を避けやすくなります。
ただし、__COUNTER__は標準C++の機能ではなく、コンパイラ拡張として扱う必要があります。
アサーションにマクロを使う
標準のassertも、マクロとして提供されています。
#include <cassert>
int divide(int a, int b)
{
assert(b != 0);
return a / b;
}
NDEBUGが定義されている場合、assertは無効化されます。
assert内に副作用を含めない
次のようなコードは避けます。
assert(initialize());
NDEBUGが定義されたビルドでは、initialize()自体が実行されなくなるためです。
処理と検証を分けます。
const bool initialized = initialize();
assert(initialized);
独自のチェックマクロを作る
文字列化と定義済みマクロを組み合わせると、条件式や位置情報を出力できます。
#include <cstdlib>
#include <iostream>
#define PROJECT_CHECK(condition) \
do { \
if (!(condition)) { \
std::cerr \
<< "Check failed: " << #condition \
<< " at " << __FILE__ \
<< ":" << __LINE__ \
<< '\n'; \
std::abort(); \
} \
} while (false)
使用例です。
PROJECT_CHECK(pointer != nullptr);
PROJECT_CHECK(index < size);
エラー処理を簡略化するマクロ
定型的なエラーチェックをまとめるために、マクロを使う場合があります。
#define RETURN_IF_FALSE(condition) \
do { \
if (!(condition)) { \
return false; \
} \
} while (false)
使用例です。
bool initialize()
{
RETURN_IF_FALSE(load_config());
RETURN_IF_FALSE(connect_database());
RETURN_IF_FALSE(start_service());
return true;
}
制御フローが分かる名前を付ける
マクロの内部でreturnする場合は、名前から動作が分かるようにします。
#define RETURN_FALSE_IF_FAILED(x) ...
#define RETURN_IF_ERROR(x) ...
#define THROW_IF_NULL(x) ...
処理内容が分かりにくい名前は避けます。
#define CHECK_RESULT(x) ...
呼び出し元の制御フローを変えるマクロは、特に慎重に設計する必要があります。
ヘッダーガードにマクロを使う
ヘッダーファイルの多重インクルードを防ぐために、ヘッダーガードが使われます。
#ifndef MYPROJECT_WIDGET_HPP
#define MYPROJECT_WIDGET_HPP
class Widget
{
public:
void run();
};
#endif
同じヘッダーファイルが複数回読み込まれても、定義は一度だけ処理されます。
#pragma onceを使う方法
主要なコンパイラでは、#pragma onceも広く利用されています。
#pragma once
class Widget
{
public:
void run();
};
標準規格だけを重視する場合はヘッダーガード、記述の簡潔さを重視する場合は#pragma onceが選択肢になります。
コンパイラごとの差異を吸収する
コンパイラ固有の属性やキーワードを統一するために、マクロが使われます。
#if defined(_MSC_VER)
#define PROJECT_ALWAYS_INLINE __forceinline
#elif defined(__GNUC__) || defined(__clang__)
#define PROJECT_ALWAYS_INLINE \
inline __attribute__((always_inline))
#else
#define PROJECT_ALWAYS_INLINE inline
#endif
使用例です。
PROJECT_ALWAYS_INLINE
int add(int a, int b)
{
return a + b;
}
ただし、指定したからといって、すべての状況で必ずインライン展開されるとは限りません。
共有ライブラリのシンボルを公開する
DLLや共有ライブラリでは、シンボルの公開指定をマクロで統一できます。
#if defined(_WIN32)
#if defined(MYLIB_BUILD)
#define MYLIB_API __declspec(dllexport)
#else
#define MYLIB_API __declspec(dllimport)
#endif
#else
#define MYLIB_API \
__attribute__((visibility("default")))
#endif
使用例です。
class MYLIB_API NetworkClient
{
public:
void connect();
};
このようなプラットフォーム差異の吸収は、マクロが有効な用途です。
機能の対応状況を判定する
コンパイラや標準ライブラリの対応状況に応じて、コードを切り替えることもできます。
__has_includeでヘッダーを確認する
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#elif __has_include(<experimental/filesystem>)
#include <experimental/filesystem>
namespace fs = std::experimental::filesystem;
#else
#error "Filesystem library is not available"
#endif
C++17より前のコンパイラも対象にする場合は、__has_include自体の存在を確認する方法があります。
#if defined(__has_include)
#if __has_include(<filesystem>)
#include <filesystem>
namespace fs = std::filesystem;
#endif
#endif
__has_cpp_attributeで属性を確認する
#if defined(__has_cpp_attribute)
#if __has_cpp_attribute(nodiscard)
#define PROJECT_NODISCARD [[nodiscard]]
#else
#define PROJECT_NODISCARD
#endif
#else
#define PROJECT_NODISCARD
#endif
使用例です。
PROJECT_NODISCARD
int calculate_result();
対応するC++標準が固定されている場合は、直接属性を書く方が読みやすくなります。
[[nodiscard]]
int calculate_result();
#errorでビルド設定を確認する
利用できない環境や不正なビルド設定を検出した場合、#errorでコンパイルを停止できます。
#if !defined(_WIN32) && \
!defined(__linux__) && \
!defined(__APPLE__)
#error "Unsupported platform"
#endif
機能設定の組み合わせを確認する用途にも使えます。
#if defined(ENABLE_SSL) && \
!defined(SSL_LIBRARY_AVAILABLE)
#error "SSL library is required"
#endif
設定の不整合をコンパイル時に検出できるため、複雑なビルド環境で役立ちます。
Xマクロでコードを生成する
Xマクロは、1つの一覧から複数種類のコードを生成する方法です。
例えば、エラーコードの一覧を定義します。
#define ERROR_CODE_LIST(X) \
X(None) \
X(InvalidArgument) \
X(NotFound) \
X(NetworkFailure)
列挙型を生成する
enum class ErrorCode
{
#define X(name) name,
ERROR_CODE_LIST(X)
#undef X
};
文字列変換を生成する
#include <string_view>
constexpr std::string_view to_string(ErrorCode code)
{
switch (code)
{
#define X(name) \
case ErrorCode::name: return #name;
ERROR_CODE_LIST(X)
#undef X
}
return "Unknown";
}
一覧を1か所にまとめられるため、列挙値と文字列の対応漏れを防ぎやすくなります。
使用後は、一時的に定義したXを#undefで解除します。
テンプレート型のカンマに注意する
プリプロセッサは、テンプレートの山括弧をマクロ引数のグループ化として扱いません。
次のコードを考えます。
#define CHECK_TYPE(type) ...
CHECK_TYPE(std::pair<int, int>);
std::pair<int, int>に含まれるカンマが、マクロ引数の区切りとして扱われます。
型エイリアスを使う
using IntPair = std::pair<int, int>;
CHECK_TYPE(IntPair);
可変長引数マクロを使う
#define CHECK_TYPE(...) ...
ただし、複雑な型処理は、テンプレートや型特性を使う方が安全です。
マクロ名の衝突を防ぐ
マクロには名前空間がないため、一般的な名前を使うと他のコードと衝突する可能性があります。
避けたい例です。
#define MAX
#define MIN
#define CHECK
#define ERROR
#define DEBUG
プロジェクト固有の接頭辞を付けます。
#define MYPROJECT_ENABLE_LOG
#define MYPROJECT_CHECK(condition)
#define MYLIB_API
Windowsのminとmaxに注意する
Windowsの一部ヘッダーでは、minやmaxがマクロとして定義される場合があります。
std::max(a, b);
Windows APIを利用する場合は、windows.hを読み込む前にNOMINMAXを定義する方法があります。
#define NOMINMAX
#include <windows.h>
ビルド設定側で定義しても構いません。
一時的なマクロは#undefで解除する
定義済みのマクロは、#undefで解除できます。
#define TEMP_VALUE 100
// TEMP_VALUEを使用
#undef TEMP_VALUE
Xマクロのように短い範囲だけで使うマクロは、使用後に解除しておくと後続コードへの影響を防げます。
#define X(name) case ErrorCode::name: return #name;
ERROR_CODE_LIST(X)
#undef X
マクロの代わりに使えるC++機能
現代のC++では、多くのマクロを型安全な機能へ置き換えられます。
定数にはconstexpr
#define RETRY_COUNT 3
inline constexpr int RetryCount = 3;
処理には通常の関数
#define DOUBLE_VALUE(x) ((x) * 2)
constexpr auto double_value(auto value)
{
return value * 2;
}
汎用処理にはテンプレート
#define SQUARE(x) ((x) * (x))
template <typename T>
constexpr auto square(T value)
{
return value * value;
}
値の集合にはenum class
#define STATUS_OK 0
#define STATUS_ERROR 1
#define STATUS_TIMEOUT 2
enum class Status
{
Ok,
Error,
Timeout
};
型の別名にはusing
#define STRING_VECTOR \
std::vector<std::string>
using StringVector =
std::vector<std::string>;
型ごとの分岐にはif constexpr
#include <iostream>
#include <type_traits>
template <typename T>
void print_value(const T& value)
{
if constexpr (std::is_integral_v<T>)
{
std::cout << "Integer: "
<< value << '\n';
}
else
{
std::cout << "Other: "
<< value << '\n';
}
}
型による処理切り替えは、プリプロセッサではなくC++の型システム上で行えます。
マクロを安全に使うためのポイント
マクロが必要な場合は、いくつかの基本ルールを意識することが重要です。
プロジェクト固有の接頭辞を付ける
#define MYPROJECT_ASSERT(...)
#define MYPROJECT_LOG(...)
関数形式マクロの引数を括弧で囲む
#define DOUBLE_VALUE(x) ((x) * 2)
式全体も括弧で囲む
#define ADD(a, b) ((a) + (b))
引数を複数回評価しない
副作用を持つ式が安全に扱えるように、可能な限り引数を一度だけ評価する設計にします。
複数回評価が必要になる処理は、通常の関数へ置き換えます。
複数文はdo-while(false)で囲む
#define DO_SOMETHING() \
do { \
step1(); \
step2(); \
} while (false)
不要なセミコロンを含めない
#define VALUE 10
次のように、定義側にセミコロンを付けるのは避けます。
#define VALUE 10;
制御フローを名前で明示する
#define RETURN_IF_ERROR(...)
#define RETURN_FALSE_IF_FAILED(...)
returnや例外送出を行うマクロは、名前から動作が分かるようにします。
実際の処理は関数へ委譲する
#define PROJECT_LOG(message) \
project::detail::write_log( \
(message), \
__FILE__, \
__LINE__ \
)
ログ出力そのものは通常の関数で実装します。
namespace project::detail {
void write_log(
std::string_view message,
std::string_view file,
int line)
{
// ログ処理
}
}
マクロの役割を必要最小限にすることで、型安全性やテストのしやすさを保てます。
マクロが適している主な場面
C++でマクロを使う価値があるのは、主にプリプロセッサ段階での処理が必要な場面です。
条件付きコンパイル
#ifdef ENABLE_FEATURE_X
// 機能Xを有効化
#endif
OSやコンパイラ差異の吸収
#if defined(_WIN32)
#elif defined(__linux__)
#endif
シンボルの公開指定
MYLIB_API
式の文字列化
#define PROJECT_CHECK(x) \
check_impl((x), #x)
ファイル名や行番号の取得
__FILE__
__LINE__
コードの定型生成
ERROR_CODE_LIST(X)
マクロの利用を控えたい場面
通常のC++機能で表現できる場合は、マクロより型安全な方法を優先します。
数値や文字列の定数
constexprやinline constexprを使います。
単純な計算処理
通常の関数やテンプレートを使います。
型の別名
usingを使います。
値の集合
enum classを使います。
型による分岐
テンプレートやif constexprを使います。
まとめ
C++のマクロは、コンパイル前の前処理段階でコードを展開する機能です。
条件付きコンパイル、OSやコンパイラごとの差異の吸収、ログやアサーション、コードの定型生成など、通常のC++機能では対応しにくい場面で役立ちます。
一方で、マクロには型や名前空間がなく、引数の多重評価や名前衝突、デバッグのしにくさといった特徴があります。
現代のC++では、目的に応じて次のように使い分けるのが基本です。
| 目的 | 推奨される方法 |
|---|---|
| 定数 | constexpr、inline constexpr |
| 単純な処理 | 通常の関数 |
| 汎用処理 | テンプレート |
| 値の集合 | enum class |
| 型の別名 | using |
| 型ごとの分岐 | if constexpr |
| 呼び出し位置 | std::source_location |
| 条件付きコンパイル | マクロ |
| OS・コンパイラ差異 | マクロ |
| シンボル公開指定 | マクロ |
| コードの定型生成 | Xマクロや外部コード生成 |
マクロを利用する際は、マクロにしかできない部分だけを担当させ、実際の処理は関数やクラスへ委譲する設計が効果的です。
この方針を意識することで、マクロの利便性を活かしながら、C++の型安全性、可読性、保守性を維持しやすくなります。
以上、C++でのマクロの活用法についてでした。
最後までお読みいただき、ありがとうございました。
