C の設計において Dennis Ritchie が行なった選択の 1 つとして、式の中で数学的に交換可能で結合可能な演算子が隣接する場合、括弧が存在する場合でも、その式を再配置する権利をコンパイラに与えました。このことは、Kernighan と Ritchie 著の『プログラミング言語 C』の付録に明示的に記載されています。しかし、ISO C は、この権利をコンパイラに与えませんでした。
この節では、前述の 2 つの C の定義間の違いを説明します。また、次のコードにおける式文を考えることによって、式の副作用、グループ化、および評価の間の区別を明らかにします。
int i, *p, f(void), g(void); /*...*/ i = *++p + f() + g(); |
式の副作用とは、メモリーへの変更と、volatile 修飾オブジェクトへのアクセスのことです。前述の式の副作用とは、i と p の更新と、関数 f() と g() 内に含まれる任意の副作用です。
式のグループ化とは、値をほかの値や演算子と結合させる方法です。前述の式のグループ化は、主に加算を実行する順番です。
式の評価には、その結果の値を生成するために必要なすべてが含まれます。式を評価するためには、指定したすべての副作用が以前のシーケンスポイントから次のシーケンスポイントまでの間で発生しなければならず、指定した演算が特定のグループ化で実行されなければいけません。前述の式の場合、i と p の更新は、以前の文からこの式文の ; までの間に発生しなければいけません。関数への呼び出しは、以前の文からその戻り値が使用されるまでの間に、任意の順番で発生できます。特に、メモリーを更新する演算子には、演算の値が使用される前に新しい値を代入しなければならないという制約はありません。
前述の式では加算が数学的に交換可能で、また結合可能であるため、K&R C の再配置の権利が前述の式に適用されます。通常の括弧と実際の式のグループ化を区別するために、左右の中括弧でグループ化を示します。この式の場合、次の 3 つのグループ化が考えられます。
i = { {*++p + f()} + g() }; i = { *++p + {f() + g()} }; i = { {*++p + g()} + f() }; |
前述のすべてのグループ化は、K&R C の規則であれば有効です。さらに、たとえば、次のように式を書き換えた場合でも、前述のすべてのグループ化は有効です。
i = *++p + (f() + g()); i = (g() + *++p) + f(); |
オーバーフローによって例外が発生するか、あるいは、オーバーフローで加算と減算が逆にならないアーキテクチャー上でこの式が評価される場合、加算の 1 つがオーバーフローしたとき、前述の 3 つのグループ化の動作は異なります。
このようなアーキテクチャー上では、K&R C では、式を分割することによって強制的にグループ化するしか方法がありません。次に、前述の 3 つのグループ化を強制的に行うために式を分割した例を示します。
i = *++p; i += f(); i += g() i = f(); i += g(); i += *++p; i = *++p; i += g(); i += f(); |
ISO C では、数学的に交換可能で結合可能であるが、対象となるアーキテクチャー上では実際にそうではない演算を再配置することは許可されていません。したがって、ANSI/ISO C の文法の優先度と結合規則では、すべての式のグループ化が完全に記述されています。つまり、すべての式は、構文解析されるとおりにグループ化されなければいけません。前述の式は、次の方法でグループ化されます。
i = { {*++p + f()} + g() }; |
このコードでもなお「f() が g() よりも前に呼び出されなければならない」、あるいは、「g() が呼び出されるよりも前に p が増分されなければならない」ということはありません。
ISO C では、予想外のオーバーフローが発生しないように式を分割する必要があります。
ISO C では、不十分な理解と不正確な表現のために、括弧の信頼性と括弧に従った評価について、間違って記述されることがしばしばあります。
ISO C の式は構文解析で指定されるグループ化を持つため、括弧は、どのように式が構文解析されるかを制御する方法としてだけ機能します。つまり、式の自然な優先度と結合規則が括弧とまったく同じ重要さを持ちます。
前述の式は、次のように書くこともできます。
i = (((*(++p)) + f()) + g()); |
グループ化と評価に与える影響は、括弧を使用しない場合と同じです。
K&R C の再配置規則には、いくつかの理由がありました。
再配置によって、より多くの最適化の機会が生まれること (たとえば、コンパイル時の定数折り畳み)
ほとんどのマシンにおいて、再配置によって整数型の式の結果が変わらないこと
すべてのマシンにおいて、いくつかの演算が数学的にも演算的にも交換可能で結合可能であること
ISO C 委員会は、記述される対象アーキテクチャーに適用されるときに、再配置規則は「as if」規則のインスタンスになるものであると、最終的に確信しました。ISO C の「as if」規則は、有効な C プログラムの動作を変更しないかぎり、実装が必要に応じて抽象マシン記述から離れることを一般的に許可しています。
したがって、すべてのビット単位の 2 項演算子 (シフトを除く) は任意のマシンで再配置できます。これは、このような再グループ化を確認できる方法がないためです。2 の補数を使用するマシンでオーバーフローが発生しない場合は、いくつかの理由のため、乗算または加算を含む整数式は再配置できます。