CPP(コンパイルしない方の関数型なC言語)プログラミング入門。とりあえずFizzBuzzまで

  • 57
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

CPPでプログラミングする記事です。
みなさんれっつCPPプログラミング!

CPPって何だよC++じゃねーのか

じゃねーんだよ。悪かったな。
いや、まあ、C++に入門する記事を期待したみなさんには申し訳ない限りです。でもBoostとかにもCPPは結構使われているので、知っていて損は無いと思います。

C言語には、マクロという機能があります。これは構文解析の前(プロプロセス時)に行なわれる単純な字句展開機能で、例えば、

cpp.c
// 「#define 名前 値」という形式で宣言する
#define MAC 42
// ソースコードにその名前が現れると、値に置換される
MAC
// 関数みたいにもできる
#define F(x) x + 20
// 関数みたいに呼び出す
F(22)

というものがあって、次のようなコマンドで実行すると、

$ gcc -E -P -C -w -o cpp.out.c cpp.c

(ちなみに、-Eというのはコンパイルを行なわずプリプロセッサーにかけることだけ行なうこと、-Pというのは#lineディレクティブを出力しないこと、-Cというのはプリプロセス後のソースコードにコメントを残すこと、-wというのは警告の出力を抑制するオプションです)

こんなファイルが作成されるはずです。

cpp.out.c
// 「#define 名前 値」という形式で宣言する
// ソースコードにその名前が現れると、値に置換される
42
// 関数みたいにもできる
// 関数みたいに呼び出す
22 + 20

#define ~な部分が消えて、マクロが展開されています。

ここで注目してもらいたいのは22 + 20のところです。ここは、プリプロセス前はF(22)となっていた部分に相当します。計算した結果の42ではなく、22 + 20となっていることから、C言語のマクロが 単純な字句置換 であることが窺えるのではないでしょうか。

さて、このマクロを使って(悪用して)、プログラミングすることをここでは CPP(C PreProcessor)プログラミング と呼ぶことにします。
また、この記事で規格と呼ぶものはC11ということにしておきます。

CPPの機能について

基本

まずは先程も書きましたが、マクロの定義は#defineで行ないます。

// 一番簡単なパターン
#define MAC1 45
MAC1 //=> 45

// 値は空白を含んでいようが何だろうが問題はない
// この場合、値は「foo bar hoge piyo」
#define MAC2 foo bar hoge piyo
MAC2 //=> foo bar hoge piyo

// #undef で未定義に戻すことができる
#undef MAC2

// 内容が複数行に渡る場合は、 \ (バックスラッシュ、円記号)を行の末尾につける
#define MAC3 Do Re Mi Fa \
             So La Ti Do!
// 展開時に改行の部分は良い感じに処理してくれる
MAC3 //=> Do Re Mi Fa So La Ti Do!

// 値の中に他のマクロが含まれていると、まとめて展開される
#define MAC4 MAC3 It is Do-Re-Mi Song!
MAC4 //=> Do Re Mi Fa So La Ti Do! It is Do-Re-Mi Song!

// しかし、文字列リテラルの中では展開されない(C言語で使うものだからね)
#define MAC5 "MAC4"
MAC5 //=> "MAC4"

ここで注意してもらいたいのは、値の中にある他のマクロが展開されるのは、 そのマクロが定義されたときではなく、展開されるとき だということです。
コードで示すと、こんな具合になります。

// 「MAC7」という値のマクロ MAC6
#define MAC6 MAC7

//この時点では MAC7 が定義されていないので、 MAC7 は展開されない
MAC6 //=> MAC7

// MAC7を定義
#define MAC7 hello

// ここでは MAC7 が定義されているので、その値である「hello」に再帰的に展開される
MAC6 //=> hello

さらに、もう一つ重要なことがありますが、それは後に説明します。

関数みたいな奴について

最初の例にもあった、関数みたいな奴についてです。関数っぽいけど明かに関数じゃないので、関数っぽい奴って呼ぶことにします。これも、基本のマクロと大体同じです。
(関数形式のマクロって呼び方が一般的で、規格ではfunction-like macroって言ってるような気がします。英語苦手だから自信ないけれど)

// 「#define 名前(引数の名前) 値」という形式で、
// 値の中にある引数の名前と同じトークンが、実際に渡された引数に置換される
#define F1(x) x * 20
F1(4) //=> 4 * 20

// 同じ引数を何度も展開したり、複数の引数を指定したりすることができる
#define F2(x, y) x * x + y
F2(20, 42) //=> 20 * 20 + 42
// ちなみに、引数の数を間違えるとエラーになるので注意
// ↑しかし1引数の関数みたいな奴を0引数で呼び出すと、空の文字列が引数になっていると見做されるのでさらに注意!

// 結構ぶっ飛んだものも引数に指定できる
#define F3(op) 20 op 4
F3(+) //=> 20 + 4
F3(*) //=> 20 * 4
F3(hours) //=> 20 hours 4
// 空白を含むことも可能と言えば可能(闇注意)
F3(+ - * /) //=> 20 + - * / 4
F3(+ 8 -) //=> 20 + 8 - 4

// 変数と同様に再帰的に展開される
#define F4(x) F3(* (x + 2) *)
F4(5) //=> 20 * (5 + 2) * 4
// ついでに。引数にマクロを渡しても展開される
// (ここの MAC1 は前項で定義したもの)
F4(MAC1) //=> 20 * (45 + 2) * 4

いくつかの例は無駄に複雑ですが、よく見るとやっぱり単なる字句置換でしか無いことに気付くはずです。

さらに、C99からは可変長引数のマクロを扱うこともできるようになっています。

// ... が可変長引数であることを表して、 __VA_ARGS__ が実際に渡された引数の内容になる
#define F5(...) __VA_ARGS__
F5()      //=>
F5(1)     //=> 1
F5(1,2)   //=> 1,2
F5(1,2,3) //=> 1,2,3

// 最低限必要な引数を指定することも可能
#define F6(x, ...) x & __VA_ARGS__
F6(1)     //=> 1 &
F6(1,2)   //=> 1 & 2
F6(1,2,3) //=> 1 & 2,3

これをどう扱うのがベストなのかは各人の判断に任せます。

ここまでのソースコードで何が起きているのか理解できた人は、これらを使うと何かすごいことができるんじゃないか、とワクワクしてくるんじゃないかと思います。任意のコードを引数にして渡したりできるわけですしね。

しかし、かなり残念なお知らせです。
実はこのマクロや関数みたいな奴は、 自分自身を再帰的に呼び出し、展開することができない(言いかえると、同じマクロを何度も展開できない) 、という致命的な弱点があります。

この弱点は、次の二つの事実を表しています。

  1. 再帰によるループができない
  2. 変数のようにマクロを更新できない

どのようなことなのか、それぞれ説明していきます。

まず1つ目の「再帰によるループができない」ということ。これがどのようなことなのかと言うと、こういうことです。

#define F7(x) F7(x + 1)
F7(10)

このコードは一見すると、F7という関数っぽい奴が無限に展開され続けて、無限ループになってしまい、停止しなくなってしまいそうです。
しかし、実際には次のようなコードが出力されるはずです。

F7(10 + 1)

直感に反しますが、 同じマクロは一度しか展開されない ということを考えれば当然の結果です。
無限ループが書けないとチューリング完全であるわけがないので、とても残念なことなのですが、そもそもコンパイルのプロセスが停止しない方がおかしな話なので、仕方のないことです。そんなに停止しないコンパイルがしたかったらD言語を使いましょう。

次に「変数のようにマクロを更新できない」とはこのようなことです。

#define VAR 1
// VAR1を更新??
#define VAR 1 + VAR
VAR

これは一体どんな結果になるのでしょうか?
正解はこうです。

1 + VAR

‥‥??
やはり直感に反する結果です。希望としては1 + 1って出てきてほしかったのですが‥‥。

なぜこうなるのかというと、まず最初にVARが展開されます。すると1 + VARになるのですが、このときもう既にVARは一度展開されてしまっていてもう展開されず、置換が終了してしまうからです。

ループの方は工夫でどうにかすることができますが、こっちはなかなかどうにもならなかったりして厄介です。

まあ、そういう厄介な問題はあとあと考えるとして、とりあえずCPPの機能の紹介に戻ります。

###演算子

この二つが果たして本当に演算子なのかいまいち確信を持てないのですが、ともかく限りなく演算子っぽいので演算子ということにしておきます。

#演算子

まずは#(ハッシュ記号、シャープではない、が一つ)の演算子について解説します。
これは単項演算子で、トークンを文字列リテラル化する力を持ちます。

具体例を示すとこんな感じです。

// 「# 名前」 という形式
#define TO_STRING(x) #x

// C言語の文字列リテラルになる
TO_STRING(hello) //=> "hello"
TO_STRING(20)    //=> "20"

// エスケープはされるものとされないものがある
TO_STRING(\x20) //=> "\x20" (エスケープされない)
TO_STRING("hello") //=> "\"hello\"" (エスケープされてる)

// 何かすごいこともできる
TO_STRING(int main(void) {
  printf("Hello, World!");
  return 0;
})

printfデバッグの際に大いに役に立つのですが、これ自体はかなりC言語に特化したものなので、今回のようなCPPプログラミングではあまり使わないような気がします。

##演算子

次に##演算子です。
こちらは二項演算子になります。結合方向? 知るか!

どのような力のある演算子なのかと言いますと、これは二つのトークンを結合する力を持ちます。
以下、例。

#define CAT(x, y) x ## y

// 二つのトークンがくっつく
CAT(hello, world) //=> helloworld
CAT(114, 514)     //=> 114514

// 空白がある場合は‥‥
CAT(hello world, goodbye) //=> hello worldgoodbye

// マクロが展開されるより早く結合してしまうので注意
CAT(MAC1, world) //=> MAC1world

//これを回避するには、もう一段マクロを噛ませればいい
#define CAT2(x, y) CAT(x, y)
CAT2(MAC1, world) //=> 42world

ちなみに単にxyを並べるだけだと、hello worldのように空白が入ってしまう、という問題があります。

#includeディレクティブ

みんな知ってるおまじない#includeです。

この#includeは2種類存在します。

  • #include <ヘッダ名>という形式。こちらは、-Iオプションで指定されたディレクトリ、C_INCLUDE_PATHで指定されたディレクトリ、コンパイラに組み込まれたディレクトリ(/usr/includeとか?)の順で探索します。
  • #include "ヘッダ名"という形式。こちらは、まずカレントディレクトリを探索したあと、上記のディレクトリを探索するようにします。

また、意外と知られていない(普通は知る必要がない)のですが、この#includeがしていることは、単純に見つかったファイルを読み込んで、ソースコードのその部分に置き換えているだけだったりします。

つまり、こんなヘッダがあったとして、

42.h
42

このように#includeで読み込んでも、ちゃんとコンパイルできますし、実行できます。

42.c
#include <stdio.h>

int main(void) {
  printf("%d",
    // この下の部分は、42.hの内容である 42 に置換される
#   include "42.h"
  );
  return 0;
}

プリプロセッサの展開をしてみれば、どういうことか分かる気がします。(その場合は#include <stdio.h>を消しておくことをおすすめします)

また、上のこと以上にさらに知られていないのですが、#includeのヘッダを指定する部分にマクロがあった場合、これは置換されてから読み込みます。

つまり、こういうことです。

#define HEADER_NAME "a.h"

// ここでは a.h を読み込む
#include HEADER_NAME

// 再定義する
#undef  HEADER_NAME
#define HEADER_NAME "b.h"

// ここでは b.h を読み込む
#include HEADER_NAME

// さらに、こんなことだってできる
#define HEADER_X(x) HEADER_X1(CAT(x,.h))
#define HEADER_X1(h) TO_STRING(h)

// "1.h"を読み込み
#include HEADER_X(1)
// "2.h"を読み込み
#include HEADER_X(2)

これは何だか悪用できそうですね。

#if#ifdefディレクティブ

これも大体の人が知ってはいると思います。

// #ifdef は定義されているかどうかで分岐する
#ifdef HOGE
  // HOGEが定義されているときに残される部分
#else
  // HOGEが定義されていないときに残される部分
#endif

// ちなみに、定義されていないかどうか、で分岐する #ifndef もある

// #if は条件式が正になるか(0ではないか)で分岐する
#if HOGE < 5
  // HOGEが5未満のときに残される部分
#elif HOGE < 10
  // HOGEが10未満のときに残される部分
#else
  // HOGEが10以上のときに残される部分
#endif

// また、 #if の条件式内のみで使える特別な関数として、 defined がある
// #if defined(マクロ名) で #ifdef マクロ名 と同じ
#if defined(HOGE)
  // HOGEが定義されているときに残される部分
#endif

// definedを使うことで、複数のマクロが定義されていることを条件にしたり、複雑な表現ができるようになる
// けど ifdef の方が楽だよね

ここで注目していただきたいのは、#ifの条件式の部分です。
この部分には、任意の整数の式を書くことができたりします。
そして、マクロも展開されます。

つまり、

#define EXPR (100 + 20)

というEXPRは普通に置換されたら(100 + 20)になってしまい、120として扱われたりはしません。
しかし、#ifの条件式の部分置いたとすると、

#if EXPR >= 100
  // EXPR の計算結果が100以上のときに残る
#endif

のように、置き換えられる際に(100 + 20)が式として解釈され、結果的に計算されることになります。

このように、#ifの条件式部分はCPPでほぼ唯一と言ってもいい数値の計算のできる場所なので、覚えておくと便利です。

定義済みマクロ達

最後に、C言語にはいくつかもとから定義されたマクロが存在します。

マクロの名前 内容
__FILE__ 現在のファイル名を表す
__LINE__ 現在の何行目かを表す
__DATE__ プリプロセスした日付を月 日 年の文字列で表す
__TIME__ プリプロセスした時間を時:分:秒の文字列で表す
predefined_macro.c
__FILE__ //=> "predefined_macro.c"
__LINE__ //=> 2
__DATE__ //=> "Apr 25 2014"
__TIME__ //=> "23:22:50"

これらは規格でも定義されているので、規格にある程度沿っていれば使えるはずです。

また、他にも規格で定義されたマクロやコンパイラが定義したものがたくさんあるのですが、ちょっと紹介しきれないので、今回は一つだけに絞ることにします。

__INCLUDE_LEVEL__という組み込みのマクロがあります。これは、現在の#includeの深さを表すマクロです。最初は0で、#includeされる度に1ずつ増えていきます。

1.h
#include "2.h"
__INCLUDE_LEVEL__
2.h
__INCLUDE_LEVEL__
include_level.c
#include "1.h"
__INCLUDE_LEVEL__

とあった場合、

2
1
0

となるわけです。

gccやclangだと定義されているようです。しかし残念なことに、MSVCでは定義されていません。
一見何の役に立つのか分かりませんが、このマクロがCPPでループを表現するのに一役買ってくれます。

CPPでFizzBuzz

やっとのことで本題に辿り着けた‥‥。長いよ‥

というわけでCPPで――つまり、

$ gcc -E -P -w fizzbuzz.c

の出力がFizzBuzzになるようなコードを書きたいと思います。

今回のFizzBuzzとは、1から100までの数値に対して、

  • 3で割り切れるときはFizzになる
  • 5で割り切れるときはBuzzになる
  • 3でも5でも割り切れるときはFizzBuzzになる
  • 上のどれにもあてはまらないときは、その数値自身

という変換を行なったものを各行に表示したもの、ということにします。
変に形式的で分かりにくいですがただのFizzBuzzだと思います。

ともかくFizzBuzzを書くには、1から100までのループができなくてはいけません。まずはそれについて考えてみます。

CPPでのループについて、再考

先程書いた通り、CPPでは単純にマクロを再帰させることはできず、ループもすることができません。

これを克服するにはどうしたら良いのでしょうか?

実は、#include__FILE__、そして__INCLUDE_LEVEL__を使うことでループを表現することができます。

このようなコードがあったとします。

loop.c
#include __FILE__

これを実行すると、一体どうなるでしょうか?

__FILE__というのは、そのコード自身のファイル名になります――つまり、このコードは自分自身を読み込むことをひたすら繰り返します。
これって、ループですよね?

ただ、無限にincludeできてしまうと停止しなくなってしまってヤバいので、includeの深さには制限がかけられています。僕の環境では200回includeしたところでエラーが出ましたが、1から100までのループでは充分な回数です。

しかも、このとき__INCLUDE_LEVEL__は自動的にインクリメントされていっているわけです。これでカウンタ変数も手に入ったことになります。やった。

じゃあ次は100回でループを打ち切るようにしよう

エラーを吐くまでループさせるのはまずいので、100回でループを打ち切るようにします。これには、#ifを使います。

loop100.c
__INCLUDE_LEVEL__
#if __INCLUDE_LEVEL__ < 100
#  include __FILE__
#endif

こうすることで、100回で打ち切られるようになります。
また。ついでに0から100まで出力するようにしてみました。

あとはFizzBuzzだぁぁぁぁぁ!!

もうここまで来たら難しいことなんてありません。
#ifでは計算が行なえるのです。%で余りを求めて、割り切れるか判定することだってできるのです。

あと、__INCLUDE_LEVEL__0のときは表示したくないので、そこで条件用のフラグを定義したりして‥‥、

こうなりました。

fizzbuzz.c
#if __INCLUDE_LEVEL__ == 0

#  define FIZZ_FLAG __INCLUDE_LEVEL__ % 3 == 0
#  define BUZZ_FLAG __INCLUDE_LEVEL__ % 5 == 0

#else

#  if FIZZ_FLAG && BUZZ_FLAG
FizzBuzz
#  elif FIZZ_FLAG
Fizz
#  elif BUZZ_FLAG
Buzz
#  else
__INCLUDE_LEVEL__
#  endif

#endif /* __INCLUDE_LEVEL__ == 0 */

#if __INCLUDE_LEVEL__ < 100
#  include __FILE__
#endif

FIZZ_FLAGBUZZ_FLAGは、置換されてから計算される、というCPPの特性をいかしたものになっています。

これを実行してみると‥‥。

$ gcc -E -P -w fizzbuzz.c
1
2
Fizz
4
Buzz
Fizz
(...まだまだ続く)

おお! CPPでFizzBuzzできた‥‥。
CPPだって、やればできるんです!

最後に

CPPプログラミング、いかがでしたか?
自身を読み込んでループする、という手法には度肝を抜かれたのではないでしょうか。gnuplotで普段からプログラミングしてるような人だと話は違うのかもしれないのですけど‥(gnuplotの最も汎用的なループ構文はrereadです)
あと、タイトルに関数型って書いたわりにあんまし関数型っぽいことしてなくてすみません。そもそもCPPは文字列書き換えシステムだよな、と思います。

また、今回はCPPプログラミングのなるべく基本的なことを薄く広く書いていったため、まだまだ足りない部分がいくつもあります。特に、__INCLUDE_LEVEL__に依存しない形で状態をどう扱うのかとか、無理矢理にでもマクロのみでループを実装する方法とか(これらは結局のところ単に力技なのですけど)。あと、##で合成したトークンを呼び出してマクロ内で分岐するとか。

そして、今回はFizzBuzzなんて(言っては難ですが)下らないことにCPPを利用していますが、実際にプログラミングする際にも役に立つことが多々あるはずです。というか、実際に役に立ちます。どんな風に役に立たせるかはここでは書きませんし、書ききれません。けれど、そういうのを考えるのは楽しいことなんじゃないかなぁ。(実装するのは結構つらいんですけれど)

最後に、こんな無駄に長い記事にお付き合いいただきどうもありがとうございました。長すぎです。どうしてC言語のプロプロセッサについて説明する記事で24kBも文章やらコードやらを書いているのでしょうか。自分の文章力の無さに辟易します。
ともあれ、この記事の内容があなたの記憶の片隅に残っていて、ほんの少しでもプログラミングをする助けになったのなら、それは僕としては幸せな限りです。

追記

今回のコードはGCCくらいでしか動かないので、MSVCでも動くようにしてみました→ http://qiita.com/alucky0707/items/d4073a9a3af9a804477a


参考文献

Cプリプロセッサメタプログラミングで、文字列系泥沼関数型プログラミング - 簡潔なQ - この記事では書ききれなかった様々なテクニックが乗っている記事です。
Boost.Preprocessor - BoostのCPPを用のライブラリです。変数っぽいものが使えたり計算ができたり、ともかく色々すごいです。ただ、実装は闇なので覗かないほうがいいです。
Cプリプロセッサ(with Boost.PP)で簡単な言語処理系を実装しよう - ここは匣 - 上記のライブラリを使って、何か怖いことしてます。ここで#include __FILE__を知りました。