世にも奇妙なマクロ展開
今回は少し趣を変えて、メタプログラミングについて少しだけ紹介してみようと思います。ウィキペディアには「プログラムを記述するプログラム」として書かれています。
もちろん、間違ってはいないですが、プログラムが自由自在に別のプログラムを書けるようになったら、もはや AI のシンギュラリティになってしまう…と思ったら最近は現実になりつつあります。
でもプログラマーが一般的にやるメタプログラミングは、特定の関数かクラスのテンプレートを1回だけ書いて、コンパイラに複数の型で使える実際のコードを自動的に作ってもらうぐらいかなと思います。
C++ で使えるメタプログラミングの方法は以下の2つになります。
- マクロ
- テンプレート
私はモダンな C++ が好きなので、マクロはあんまりお勧めできません。でもちゃんと理由がありますよ!
特定の例で比較してみましょう。
マクロ
以下のマクロはみなさんも色んなコードで見たことがあると思います。
#define min(a,b) ((a) < (b) ? (a) : (b))
#define max(a,b) ((a) < (b) ? (b) : (a))
こういうマクロを見ると、マクロを使いたくなるのは分かります。一行でminとmaxをどの型でもコールできるようにするって凄いですよね。
効率的に一番いいと思います。自分だけ使うコードの場合は、全然マクロでいいと思います。でもコードを提供する場合は、マクロは良くないと思います。
マクロはコンパイル時に文字列を置き換えるだけの機能なので、そこに何もロジックがないです。
上記のminとmaxのaとbに何かを指定すると、それを文字列としてを右側のaとbと書いてあるところにコピペしてくれるだけです。
マクロのお陰で、普通のプログラミングではできないこともできてしまいます。
例えば、以下のように変数名を出力できます。
#define print_name(x) std::cout << (#x) << std::endl
int test = 5;
print_name(test); // "test"
これはただの文字レベルでの置き換えだからこそできることで、正式な方法で変数名を出力するには、リフレクションという本プログラムから自分自身の構造にアクセスする機能が必要になりますが、C++ にはそういった機能はないです。
マクロには関数や変数、型などの概念が存在しないです。
そのため、以下の2つの問題が発生します。
- 本当に何でもコールできてしまいます。例えば、
aがbool型で、bがchar型でもいいです。要するに、aとbが<演算子が使える型だったら、何でもいいです。aとbには制限をかけられないです。例えば、「同じ型の変数でしか呼べないようにする」という制限はかけられないです。 - C++ の式の通常の評価と違う挙動になるケースがあります。
以下のコードでminが何を返すと思いますか?
int x = 0;
int y = 100;
int z = min(++x, y);
C++ 的には、先にxに1が足されるということで通常は1になると思いますが、実は2が返されるのです。マクロは渡される文字列をコピペするだけなので、以下のようになります。
min(++x, y)
// マクロが展開されるとこうなります↓
((++x) < (y)) ? (++x) : (y)
++x がそのまま2回コピペされて、2回実行されてしまい、xに1が2回も足されてしまいます。
この場合は、minの定義に括弧を付けてもこの問題は回避できないです。
もう1つの例として、以下のコードでCIRCLE_AREA()は何を返すと思いますか?
#include <numbers>
#define CIRCLE_AREA(radius) radius * radius * std::numbers::pi
float result = CIRCLE_AREA(1 + 1);
おそらくこれを書いたプログラマの期待は「1 + 1が評価されて、CIRCLE_AREA()には2として渡され、4 * piになる」だと思いますが、マクロではそのまま置き換えられるので、1 + 1 * 1 + 1 * piになり、2 + piが返されます。
どちらのケースも、書いたプログラマの期待とは異なってると思います。
テンプレート
minとmaxをテンプレートを使って実装すると、以下のようになります。
template<class T>
const T& min(const T& a, const T& b)
{
return (a < b) ? a : b;
}
template<class T>
const T& max(const T& a, const T& b)
{
return (a < b) ? b : a;
}
もう少し複雑になっていると思いますが、実は標準ライブラリのstd::minとstd::maxもテンプレートで実装されています。
マクロ版での2つの問題が解消されているかを見てみましょう。
- こちらの
minとmaxは同じ型のaとbでしかコールできないです。aがint型で、bがlong型という暗黙的キャストされる型でもコンパイルエラーになります。違う型で使いたい場合は、ユーザーが明示的にキャストしないといけなくて、ユーザーの責任になります。 - こちらの
minを以下のようにコールしてみましょう。
int x = 0;
int y = 100;
int z = min(++x, y);
こちらは、ちゃんとxが1足されてからminに渡されるため、期待通り1が返されます。
まとめ
纏めると、誰かに提供するようなコードでは、ちゃんとテンプレートを使って、ユースケースに合わせて型に制限をかけて、作らないといけないですが、テンプレートよりマクロの方が書きやすいため、自分だけが使うコードのような、正しい使い方が保証されている場合は、マクロをいくら使ってもいいと思います。
ヘッダーで API として、つまり、グローバルの範囲で使うのはテンプレートで、cpp の中でローカルの範囲で使うのはマクロでいいと思います。
ちなみにc++には C++11 から型をチェックする機能が追加されていて、テンプレートでも型のチェックができるようになっています。チェックはいろいろ自分でも作れます。さらに、C++20 からはConcepts libraryでそういったチェックをもっと分かりやすく定義できるようになりました。
こちらもすごく便利な機能だと思います。この記事に書くと長くなりそうなので、また別の機会に紹介したいと思います。
Discussion
「テンプレートに制限をつけること」ができるようになったのは言語機能の追加によるもので、「Concepts library」はあくまで基本的なコンセプトを提供するものである事に留意したほうが良いかもしれません。
引数
abはカッコで括った方が良いし行末のセミコロンは無い方が良いと思います。マクロの主な使い道について、C++でお世話になるのはおよそ
#という文字列化演算子と##トークン連結演算子と処理系指定の値(バージョンやソース行番号など)を受け取る用のマクロくらいと思います(それ以外の実装を端折るためのマクロも実際にはよく使われるが、必須ではない)。記事でもおよそそういう形になってるのは良いのですが、書くなら文字列化演算子の説明はしないといけないし、可能ならプリプロセスの話もした方がいいでしょう。記事内のサンプルコードをちょっと変えて、動かして確認する形にした実例だけ載せておきます。
左のペインがソース、真ん中がプリプロセス結果、右のペインが実行結果になります。プリプロセス結果を見るときはincludeをコメントした方がいいです。