##本記事の前提
本記事中のコード例で使用する式X
及びY
は、以下のAND条件を満たすことを仮定します。
- 単項式であること。
- 式の評価結果は変更可能な左辺値であること。
- 評価結果の値はスカラ型であるか、スカラ型でもポインタ型の場合、オブジェクト型へのポインタ型であること。
また、「変更可能な左辺値」等の用語の定義並びに規定は、C90の規格票(「プログラム言語C JISX3010-1993 (ISO/IEC 9899:1990)」)に準じますが、抽象意味規則を除く記述(式解釈の手順など)など規格の守備範囲を超えるものについては、執筆者独自の「たぶんこうかな~」という思いつきに拠るものです。
##式 X+++++Yの解釈
「C言語プログラミングの落とし穴」(A・コーニグ)にも出てきましたが、以下のいやらしい式の解釈方法について考えてみます。
X+++++Y;
まず、この式を有効前処理字句最長一致ルール1を適用してトークン分割すると、以下のようなトークン分割が得られます(半角スペースは、トークン区切りのプレースホルダーを示す)。
X ++ ++ + Y;
直観的に考えると、二項演算子+
より前置++
演算子の方が優先順位が高いのだから、以下のようなトークン分割がされるんじゃないの?と思いますが、字句解析と構文解析は分業で行われるため、トークン分割が有効前処理字句最長一致ルールで行われる以上、以下の区切り方はトークン分割のフェーズにおいては検討にさえ挙がってこないのではと思います(僕はコンパイラの勉強をしたことが無いので推測で言ってますが・・・)。
X++ + ++Y;
ところで、この段階ではトークンを区切っただけであり、以下が未決事項です。
- 2つの演算子
++
は、それぞれ前置の++
か?後置の++
か? - 演算子
+
は、単項+
演算子か?二項+
演算子か?
ここで、演算子の優先順位と結合法則を適用します。
上記の式において、前置の++
と後置の++
では、後置の++
の方が優先順位が高いため、演算子++
は両者とも後置の++
であると解釈されます。また、式中では演算子+
は左オペランドと右オペランドが指定されているため、二項演算子の+
であると特定します。以上より、以下の式と等価になると考えられます。
((X++)++) + Y;
上の完全式を評価してみる前に、ここで演算子の仕様についておさらいしてみます。
- 式
X++
は右辺値を返す。 - 後置
++
演算子は、オペランドに変更可能な左辺値を要求する。
式中の左から2つ目の後置++
の評価時点で、上記2の制約に違反してしまうことから、実際の処理系では構文解析の時点でコンパイルエラーが発生するのではと予測します。実際、gcc 4.9.2
でコンパイルしてみると、以下のエラーメッセージが発生しました(インデントの関係上、式中のエラー発生箇所を示す記号を^
から*
に変更しています)。
error: lvalue required as increment operand
X++*+++Y;
以上より、冒頭の式を規格合致処理系でコンパイルした結果はどうなると推論されるか?という問いに対しては、「後置++
演算子はオペランドに変更可能な左辺値を要求する」という制約に違反するため、コンパイルエラーが発生する、が回答になると思います。
##まとめ
前置++
と後置++
のように、種類の異なる演算子に同じ記号が割り当てられていた場合、どの演算子として扱うか?という演算子アイデンティファイの問題と、演算子の種類が決まったとして、オペランドがどの演算子のオペランドに属するか?という演算子の優先順位と結合法則の問題などが同時に絡んでくるという、複雑ですが学ぶところの多い式だと思いました。23
式を読む上でのポイントをまとめると、以下の点が挙げれられると思います。4
- 有効前処理字句最長一致原則に従ったトークン分割
- 演算子のアイデンティファイ(トークン分割した結果、異なる種類の演算子に紐づいたトークンが表れた場合、各々が何の演算子を意味するかの特定作業)
- 演算子の優先順位・結合法則の適用
以上です。
-
「有効前処理字句最長一致ルール」とは本記事の独自用語ですが、「入力ストリームをある文字まで前処理字句に解析し終わったとき、次の前処理字句は、前処理字句を構成することのできる最も長い文字の並びとする。」のことを指しています。 ↩
-
なお、演算子の優先順位・結合法則の話と、オペランドの評価順序の話は別の議論です。前者はオペランドの取り合いの話であり、後者は時系列の話だからです。また後者には関連テーマとして「未規定の動作」「副作用完了点」(式の評価にともない発生した副作用が完了するタイミング)が絡んできます。 ↩
-
「同じ記号で表されるが意味の異なる演算子」を調べたところ、前置/後置増分演算子の
++
、単項/二項演算子の+
/-
、間接演算子/二項*
演算子、アドレス演算子/ビット単位&
演算子、キャスト演算子/関数呼び出し演算子()
、がありました。 ↩ -
考慮すべきはもちろんこれだけではなく、8段階で構成される「翻訳フェーズ」も考慮する必要があります。 ↩