その1 「セミコロン」

文の終わり?

C言語ヨタ話、まず最初はセミコロンについてです。

初心者向けのCの入門書なんか読むと、よく以下のような記述が見られます。

C言語では、文の終わりには、セミコロンを付けます。

もちろんですね。

いえ、もちろん、初心者向けに 「文の終わりにはセミコロンを付けるんだよー」 と説明することは、何ら悪いことではないと思います。

ただし、例えば、「if文」はもちろん「文」ですが、

  if (a == 0) {
      /* なんとかかんとか */
  } /* ←ここ */

こういう所にはセミコロンは付けません。 でも、同じ制御構造を表わす文でもdo whileの後には必要、 じゃあ中括弧('}')の後には不要なのか、と思ってると、 配列・構造体の初期化子の後には必要、 構造体定義の後にも必要、でも、 関数定義の後には不要、 余談ですがC++のクラス定義の後にはセミコロンが必要なのに、 Javaのクラス定義の後には不要だったりします。

ちなみに、K&Rには、以下のように書いてあります(p.67)。

Cにおいてはセミコロンは文の終了を示す。

これもやっぱり嘘でしょうね。ただ、K&Rの方は、 その少し下に、

ブロックの終りを示す右大カッコの後にはセミコロンは付けない。

と書いてあります。なんか余計混乱を招きそうですが。

# '{}' って、K&Rの日本語版では「大カッコ」と訳されていますが、
# その昔算数で習ったところでは、「中括弧」だったと思うのですが。
#   小括弧... '()'
#   中括弧... '{}'
#   大括弧... '〔〕'
# じゃなかったかなあ...
# 今回、この文章中では、引用部分を除いて「中括弧」で統一しようと思います。
# 2000/6/15補足
# これについてメイルでご指摘いただきました。欧米では、
# '{〔()〕}' という順序で使うそうですね。
# 知らなかったです。学のなさバレバレ (^^;

結論を言えば、あくまで文法的に見るなら、 Cの場合、セミコロンは、文の一部なんですよね。

K&Rなら、p.295から、BNFもどきによるCの構文規則が記載されています。 それによると、例えば式文(主に関数呼び出しとか代入とか)なら、

expression-statement:
      expressionopt ;

意訳すると、「式文(expression-statement)」とは、「式(省略可)」に セミコロンをくっつけたものである、ということです。 ちなみに、「式」がopt(optional--省略可能)になっているのは、 セミコロンだけの空文が許されているからですね。 空文は式文の一種という分類みたいです(JIS-X3010 6.6.3は、 「式文及び空文」というタイトルだけど)。

中括弧で囲まれた文は、

compound-statement:
      { declaration-listopt statement-listopt }

こうなってますね。つまり、宣言の並びと、文の並びを、 中括弧で囲むと、compound-statement(複合文)と呼ばれる ひとつの文になる、ということです。 statementについての規則を見るとわかりますけど、 複合文は文の一種です(複合文は、「ブロック」と呼ぶこともあります)。

ここでわかることは、複合文がセミコロンを含んでいない、 ということです。

if文等は、statementで終わるので、最後に付く statementにセミコロンが付くかどうかによって、 if文自体にセミコロンが付くかどうかが決まります。

selection-statement:
      if ( expression )  statement
      if ( expression )  statement else statement
      switch ( expression ) statement

ついでに、以下のような宣言におけるセミコロンですが、

  int a;  /* ←ここのセミコロン */

まず、「宣言」は文ではありません。 そして、「宣言」は以下のように定義されています。

declaration:
      declaration-specifiers init-declaration-listopt ;

このように「宣言」にはセミコロンがくっついているので、 構造体定義とか、配列の初期化の際には、中括弧の後だろうと何だろうと、 セミコロンが必要になるわけです (宣言の構文規則については、あんまり深入りしたくないです... なにせもう、どーしよーもないほど、救いよーもないほどアレなもので :-p)。

Cだと、もう一箇所、セミコロンが登場する場所があって、 それは、for文の()の中です。これは、forの構文規則の中に、 別途定義されています。

まとめると、Cでは、以下の場所でセミコロンを使うことになります。

で、困ること

このように、Cにおける「文」は、文自体がセミコロンを含んでいるわけですが、 「複合文」は、後ろにセミコロンが付きません。

文の種類によってセミコロンが付いたり付かなかったりすることによって、 たまに問題が起きることがあります。 例えば、以下のように、ふたつの整数型変数の値をひっくり返すマクロを 作ろうとしたとして、

#define SWAP(a, b) {long temp; temp = a; a = b; b = temp;}

普段は以下のように使うのでしょう。

  SWAP(a, b);

この場合、マクロを展開したコードは、

  {long temp; temp = a; a = b; b = temp;};

このようになり、 本当なら中括弧の後には要らない筈の セミコロンが入っちゃっています。 ま、この場合は、余計な空文が入っていると解釈されるだけで、 別に害はないんですが、

  if (...)
      SWAP(a, b);
  else
      ...

このように使ってしまった時、展開すると、

  if (...)
      {long temp; temp = a; a = b; b = temp;};
  else
      ...

こうなってしまいます。elseの前にセミコロンが入って、 if文が終わったと解釈されてしまうので、これはシンタックスエラーです。

SWAPマクロは、関数呼び出しのような感じで(つまり式文として)使いたい、 でも、式文にはセミコロンが含まれるが、 複合文にはセミコロンが付かないという、Cの文法の対称性のなさが、 ここで問題になってしまっているのです。

これを避けるtipsとしてよく知られているのが、以下の方法です。

#define SWAP(a, b) do {long temp; temp = a; a = b; b = temp;} while (0)

確かにこれで回避はできますけど... こりゃ美しくないです。

# さらに細かいことを言うと、コンマ演算子で式を並べる場合、
# 上記のSWAPでもやっぱり問題を起こすのですけれども。

assert()みたいに、条件によって消え去ってしまうマクロは、 例えばFreeBSD3.2の/usr/include/assert.hでは、以下のように定義されてます。

#ifdef NDEBUG
#define	assert(e)	((void)0)

((void)0)でなくて「無」に置換してしまうと、 同様の問題が発生するからですね。

# 2000/5/10補足
# この場合の「同様の問題」というのは、コンマ演算子で式を並べる場合の
# 問題のことですね。文脈から言って、どこも「同様の問題」じゃないですね。
# これ書いたとき、なんか勘違いしていたようです。一応補足します。

余談ですが、「SWAPなんて一時変数使わなくったってできるじゃん」と 以下のようなマクロを挙げる人もいるかも知れませんけど、

#define SWAP(a, b) (a += b, b = a - b, a -= b)

この手のワザ(他にXORを使う方法もある)は、 同一の変数をひっくり返す場合には正常に動作しません。 SWAP(a[i], a[j])と書いてて、たまたま i == j だったりするとはまりますね。 そういうことがあり得ない局面なら、使ってもかまわないと思いますが。

Pascalの話

K&R p.67の、さっきの記述の続きには、こんなことが書いてあって、

これはPascalのような言語におけるセパレータではない。

これはその通りです。 Pascalの構文規則は、以下のようになっています (JIS X3008 「参考1 構文規則」より)。

  文並び = 文 { ";" 文 } .

K&RにおけるCの構文規則とは、ちょっと記法が違ってますが、 要は、「文並び」とは、「文」の後に任意個(ゼロ含む)の 「";" 文」をくっつけたものだ、という意味です。

つまり、Pascalでは、

  文 ; 文 ; 文 ; 文 ; 文

のように、文と文の間に(セパレータとして)セミコロンが入ります。 Cと違って、文自体はセミコロンを含みません。

この構文規則、規則だけ見れば、確かに美しいんですが、 いざ実際に書いてみると...

  文1;
  文2;
  文3

最後の文の後ろにはセミコロンが不要なので、このように書くわけですけど、 これだと、文3の後ろにさらに文を追加するとき、 いちいちその前の文にセミコロンを付加しなければならないので、 結構面倒臭いです。

というわけで、Cに慣れた人は、 Pascalでもセミコロンだけの文は空文であることを利用して

  文1;
  文2;
  文3;

こんなふうに書いたりするわけですけど、これまた if else の時には問題になったりします。

  if 条件式 then
      文1; {←ここで文が終わっちゃう}
  else
      文2;

文1の後のセミコロンで文が終わってしまうので、 シンタックスエラーになってしまいます。

セミコロンを文の終わりに置く(Cの場合正確にはそうじゃないけど)言語と、 文の間に置く言語の間では、文の終わりに置く言語の方がミスが少ない、 ってことを、確かどっかの機関が統計とって実証してたと思います。 詳細は知りませんが。

ただ、私も、C++で、アクセサをインラインで書く時など、 セミコロンを忘れたことがあったりします。

  double getX() {return x;}; /* return文の後のセミコロンを忘れる */

感覚的には、セミコロンは、実は「文の終わり」ではなく、 「行の終わり」なのかも知れません。 で、「継続行」の場合には、セミコロンを書かずに次の行に続ける、と。

# 2000/2/13補足:
# 実は公開当時、上記のSWAPマクロの定義の所で、全部最後の中括弧の前の
# セミコロンを書き忘れてました(^^; やっぱり、少なくとも私にとっては、
# セミコロンは「行の終わり」であるようです。

脱線しますけど、Cの場合、配列の初期化子の最後の要素の後ろには、 コンマを付けても付けなくても良いことになっています。

  char *a[] = {
      "hoge",
      "piyo", /* ←ココ */
  }

この規則、嫌いな人もいるようですが、私は結構気に入ってます。 Pascalで最後の文の後にセミコロンを付けるのと同じ理由からです。 後ろに追加するときに楽なので。

# ANSI-C Rationaleでは、この規則は、追加/削除を容易にするためと、もうひと
# つ、コードを自動生成するプログラムを簡単に書けるようにするために、この
# 規則を入れたことになってます(3.5.7)。
# これが嫌いな人の意見として私が聞いた(読んだ)ことあるのは、「エキスパート
# Cプログラミング」のp.66に書いてあるものなのですが(この本の著者のPeter
# van der Lindenさん、よっぽどこれが嫌いみたいで、「Just JAVA」でも同じ
# こと言ってます)、この本には、「コードを自動生成するプログラムで最後の
# コンマを出さないようにするなんて簡単じゃないか。ほらほら」という感じで
# サンプルコードが載せられているんですけど、そのコードが、ローカルなstatic
# 変数を使った「1回しか使えない」タココードじゃあ説得力ないよなあ...

でも、どうせなら、列挙の宣言でもそのようになっていなければ、 片手落ちだと思うのです(コンパイラによっては、何も言わずに通したり、 警告で済ませたりするようなんですが)。

  typedef enum {
      BLACK,
      BLUE,
      RED,
      ...
      WHITE  /* ←ここにコンマを付けるとエラーなんだこれが */
  } Color;

でも、ま、列挙でループするときに備えて以下のように書く人には、 関係ないかも知れません。

  typedef enum {
      BLACK,
      ...
      WHITE,
      COLOR_NUM
  } Color;

ただ、私の場合、列挙は明示的に1から始めるようにしてます。 これは、なんかのミスで初期化を忘れた場合など、 そこには確率的にゼロが入っていることが多いような気がする (さもなきゃとてつもなく大きな値か)ので、 デバッガでバグを追うとき、ミスを検出しやすいと思うからです。 その場合、最後の要素は、COLOR_NUMにはならないので、

  typedef enum {
      BLACK = 1,
      BLUE,
      ...
      WHITE,
      COLOR_NUM_PLUS_1
  }

こんな感じになっちゃいますねえ。

ただ、COLOR_NUMにせよ、COLOR_NUM_PLUS_1にせよ、 こういう余計な列挙子を付けとくと、switch caseで場合分けするとき、 コンパイラによっては警告を出してくれるのもあったりして (gcc の -Wall だと出しますね)、無駄なcaseとassert(0)を 書くことになったりします。 この警告は非常に有用なので、抑止するわけにもいきませんし。

  switch (color) {
    case BLACK:
      ...
      break;
    case COLOR_NUM_PLUS_1:
      assert(0);
      break;
    default:
      assert(0); /* ←私はここには必ずこーゆーのを書く */
  }

では、どうあるべきだったのか?

えー、プログラミング言語について「かくあるべき」というのは、 多分に主観の問題で、故に宗教戦争になることが非常に多いわけですけど、 わたしゃ宗教も宗教戦争も大嫌いなので、 「以下は私の主観です」と予防線をまず張っときます(笑)。

ついでに、私は別に今更Cの文法を変えて欲しい、 とか考えているわけではなくて、 既に妄想モードであることも先にお断わりしておきます。

んで、では、Cのセミコロン周りの文法が、 「どうなっていたら良かったのかなあ」ということをつらつら考えますと、

まず、Pascalみたいに、文と文の間にセミコロンが入るのは不便です。 この点では、「おおむね」文の終わりにセミコロンが来る、Cの文法の方が 良いと思えます。

でも、文の種類によって、セミコロンが付いたり付かなかったりする、 という対称性のなさはどうも気に入りません。となれば、 複合文の後にもセミコロンを付けるようにする、という案が考えられます。

  if (a == 0) {
      ...
  };

私には、特に違和感ないです。

でも、

  if (a == 0) {
      ...
  }; else {
      ...
  }

さすがにこりゃちょっとヘンなような気がします。 やっぱり、セミコロンは「行の終わり」なのかも知れません。 インデントの流儀によっては、違和感なくなる気もしますし。

ところで、考えてみれば、さっきから問題になるのは、 「elseの直前」ばかりです。

こりゃ if elseの構文の方を疑ってみる必要があるぞ、ってなわけで、 わかる人にはもうオチは見えたと思うんですが、 Cでは「ぶら下がり文(dangling statement)」を許している所がまずいわけですね。

「ぶら下がり文」とは、if文やwhile文などで、文がひとつの時に、 中括弧を付けずにぶらりんとぶらさげて書かれた文を指します。

  if (...)
      a = 0; /* ←この文がぶら下がり文 */

Cのif文は、構文規則(すなわちコンパイラの解釈)では、確かに

  if ( 条件式 ) 文  else 文

で、複数の文を書きたければ中括弧で囲んでひとつの文にしなさい、 ってことなんですが、現実にコーディングする人間の立場からすれば、 「文がひとつの時は中括弧を省略しても良い」と思っているのが現実です (入門書などでは時々そういう記述を目にします)。

実際のプロジェクトでは、以下のようなポカ(こりゃちょっとアホですが)

  if (...)
      a = 0;
      b = 0; /* ←文を一個追加した */

とか、以下のような曖昧さ(K&Rに載ってるCの構文規則をyaccにかけたとき、 唯一出るconflictがこれですね)

  if (...)
      if (...)
          ...
  else
      ... /* ←インデントに騙されちゃいけませんぜ */

とかを避けるために、「中括弧省略禁止」というコーディング規約を 設けている所も少なくありません(複数回経験アリ)、 自分で自分を縛っている人も多いでしょう。 私もおおむねそうです。 ぶら下がり文は、breakとかreturnとかだけの時、ぐらいしか使いません。

それならいっそ構文規則を変えて 中括弧を省略不能にしてくれた方がなんぼかスッキリします。

  if ( 条件式 ) { 文並び } else { 文並び }

こんな調子ですね。

行数が増えるのが嫌なら、

  if (a == 0) {...}

こう書いても別にいいわけですし。

タイプ量が増えるのがヤダ、という意見もあるかも知れませんが、 たった2文字のタイプがどれほどの手間やねん、という気もしますけど、 中括弧を前提にできるなら、条件式を囲む方の括弧は不要になりますね。

  if a == 0 {
      ...
  }

もはやCとは似ても似つきませんが(^^;

Pascalではぶら下がり文を許していますが、Pascalの後継(と言っていいか?)の Modula2や、さらにそれを発展させたAdaなどでは、 ぶら下がり文を認めていません。 だから、beginを書かないようになっています。Rubyもそうですし、 そういえばVBもそうですね。

中括弧文化の言語では、例えばPerlはぶら下がり文を認めていません。 でも、Javaは放っておくとしても、 少なくともJavaよりは構文に改善が加えられたと思えるLimboでも、 なぜかぶら下がり文を認めてますね。うーむ。

補足ですが、ぶら下がり文を禁止すると、else if が使えなくなります。 よって、ぶら下がり文を禁止した言語では、elifとかelsifとかの キーワードを追加して、

  if (...) {
      ...
  } elsif (...) {
      ...
  } else {
      ...
  }

というふうに書けるようにしてますね。

ところで、Cプログラマの中には、else if について、 C言語に特別にそういう構文が用意されている、と思っている人が、 どうもちょくちょくいるようです。 もちろん勘違いです。単にelse の後にぶら下がり文で if文がくっついているだけです。

そういえば、if文には中括弧を必ず付けるべし、みたいな コーディングルールを適用しているプロジェクトでも、 「else ifは例外とする」と コーディングルールに書いてあるのは見たことないな(笑)。

さて... セミコロンの話をしていた筈が、 何故かぶら下がり文の話になっちゃいました。 んで、ぶら下がり文を認めないことを前提にしてしまえば、 中括弧の後にセミコロンを付けようが付けまいが、 どっちでも良さそうですね。ありゃりゃのりゃ。

でも、私としては、やっぱり「文の終わりにセミコロン」という 単純な文法の方がいいような気がします。

  if (...) {
      ...
  } else {
      ...
  };

こんな調子ですね。Adaだと、中括弧ではないですが、 こんな感じになっているようです。

for文の中のセミコロンは例外ってことになっちゃいますが。

おわりに

なんか初回から、文法の「重箱の隅」を突っついたような気もしますけど、 ま、ヨタ話ということで(^^;

でも、全くの初心者がプログラミング言語を学ぶ時って、 「セミコロンの付け所」みたいな些細な所で、結構はまるものです。 #includeの後にセミコロンを付けちゃったり、 do whileの後にセミコロンを忘れたり、というのは、 私もやった記憶があります。 逆にJavaのimportの後にセミコロンを忘れたりという例も 見たことがあります。

そういう初心者さん達には、このヨタ話もちょっとは参考に... ならんかなあ、やっぱり。


ひとつ上のページに戻る | ひとつ後の話 | トップページに戻る