古代C言語で1=2を証明してみた


少し前に「(a== 1 && a ==2 && a==3)が常に真にできますか?」1というのが、流行ったと思います。
今回は「1 == 2」を証明している記事をみたので、これを扱っていきます。
(クイズ形式でやっていきたいと思います。初心者がやっても面白いように書いている(はず)。)

なぜこの記事を書いたか

1=2 - アンサイクロペディア

この記事は、名前の通り「1と2は等しい」ということをいろいろな方法で証明している記事です。
それにしても長い記事なのです。もう「1 = 2」の証明だけで本が1冊書けるかもしれないですね。

かかってこいや!「1=2の証明」で鯵坂もっちょさんが、数学を用いた「1 = 2」の証明における間違いを片っ端から指摘しているのを見て、面白いなと思いました。

内容はタイトルの通り、「1と2は等しい」ということをあの手この手で証明してみた記事なのですが、それにしてもこの記事が面白いのです。もちろん、数学的には一つ残らず間違っているわけですが、「どこが、なぜ間違っているのか」を考えるととても勉強になるような項目もあって、間違いだからといって簡単には見過ごせません。
よっしゃ! だったら片っ端から間違いを指摘してやる! かかってこいや!

よっしゃ、だったら僕も古代C言語2を用いた証明の間違いを片っ端から指摘してやる!
というわけで、さっそく。

注意:以下ではアンサイクロペディアから説明文や、ソースコードを引用しています。その際、見にくいコメントや、ソースコードは勝手に削除したり、整形したりしていますので、そこはご了承ください。

C言語による証明その1

C言語を使っても、1=2を証明できる。
以下は「1=2ならYESを表示しろ」という趣旨のプログラムである。このプログラムを実行すると、YESを表示する。したがって「1=2」である。

C言語による証明その1
 #include <stdio.h>

 int main(void) {
    int a = 1, b = 2;
    if (a = b) puts("YES");
    return 0;
 }
実行結果
YES

やったぜ

証明その1の答え合わせ

あなたの思っている等価演算子は等価演算子ではない
これは初歩的なミスですね。
プログラミングをやっている人なら、誰しも分かります。

左の値が右の値と等しいか判断するには、「等価演算子」という演算子を使います。
そして、C言語における等価演算子は=ではなく==です。
=は代入演算子といって、左の変数に右の変数の値を代入します。
C99の仕様書によると、if文は「式の値が 0 と比較して等しくない場合,最初の副文を実行する。」ということが書かれています。
一方、代入演算子は「代入式は,代入後の左オペランドの値をもつ」ということが書かれています。
つまり、a = babの値(2)が代入され、変更されたaの値(2)が返ってきます。
よって、if (a = b)if (2)であり、式の値が 0 と比較して等しくないので、最初の副文「puts("YES");」が実行され、YESが表示されたというわけです。

証明その1の傾向と対策

証明その1の対策
#include <stdio.h>

int main(void) {
   if (1 = 2) puts("YES");
   printf("%d", a=b);
   return 0;
}

このように直接やれば、

main.c:4:10: error: expression is not assignable
   if (1 = 2) puts("YES");
       ~ ^

コンパイラに「expression is not assignable(ふぇぇ、式には代入できないょぉ……)」というお叱りの言葉を授かり、自分の犯した過ちに気づくことができるはずです。

そもそも、もともとのソースコードでも、

p1.cpp:5:11: note: use '==' to turn this assignment into an equality comparison
    if (a = b) puts("YES");
          ^
          ==

note: use '==' to turn this assignment into an equality(等価演算には==を使うんだからねっ!…べ、別にあなたのために警告したわけじゃないんだからね、勘違いしないでよね!)」とコンパイラ様のありがたいお言葉を授かっているはずです。
教訓その1:ちゃんと警告は読みましょうね。

C言語による証明その2

以下は「0.1の10倍が0.2の10倍に等しければYESを表示しろ」という趣旨のプログラムである。このプログラムを実行すると、YESを表示する。よって「1 = 0.1の10倍 = 0.2の10倍 = 2」である。

C言語による証明その2
 #include <stdio.h>

int main(void) {
   int a = 0.1, b = 0.2;
   if (10 * a == 10 * b) puts ("YES");
   return 0;
}
実行結果
YES

はい、優勝(コロンビア)

証明その2の答え合わせ

あなたが思っているほどコンピュータは数を表現できない
これも基本的なことを間違えているがために、このような結果になっています。

まず、int型は整数型なので、整数しか扱えません。
小数点以下は切り捨てられます。
つまり、最初の時点でaには0が代入され、bには0が代入されます。
…もうお分かりですね。
if (10 * a == 10 * b)
if (10 * 0 == 10 * 0)
if (0 == 0)
if (1)
これにより、最初の副文「puts("YES");」が実行され、YESが表示されたというわけです。

証明その2の傾向と対策

p2.c:4:12: warning: implicit conversion from 'double' to 'int' changes value from 0.1 to 0 [-Wliteral-conversion]
   int a = 0.1, b = 0.2;
       ~   ^~~
p2.c:4:21: warning: implicit conversion from 'double' to 'int' changes value from 0.2 to 0 [-Wliteral-conversion]
   int a = 0.1, b = 0.2;
                ~   ^~~

もちろん今回も、コンパイラが、「warning: implicit conversion from 'double' to 'int' changes value from 0.1 to 0(先輩っ><! doubleからintに黙って変換したから、値が0.1から0になっちゃってますけど、これって大丈夫なんですか??)」とさりげない(警告という名の)優しさを振りまいてくれているはずなので、ちゃんと受け止めてあげましょう。
教訓その2:ちゃんと警告は読みましょうね。(大事なことなので二回言いました)

C言語による証明その3

以下は「(2-0)×(2-1)を計算しろ」という趣旨のプログラムである。このプログラムを実行すると、1を表示する。よって「2 = (2-0)×(2-1) = 1」である。

C言語による証明その3
#include <stdio.h>

#define A 2 - 0
#define B 2 - 1

int main(void) {
   printf("%d", A * B);
   return 0;
}
実行結果
1

今回は、コンパイラが警告を出しませんでした。
…ということは!?

証明その3の答え合わせ

あなたの思っているような式は展開されない
#define A Bはマクロ定義といって、マクロ名Aを式Bに変換します。
ただし、変換といっても、ただの置換になります。
よって、
printf("%d", A * B);は、
printf("%d", 2 - 0 * 2 - 1)となります。
演算子は掛け算から優先されて計算されるので、
printf("%d", 2 - 0 - 1)となります。
printf("%d", 2 - 1)
printf("%d", 1)となり、「1」が出力されます。

証明その3の傾向と対策

マクロ定義は便利な一方で、正しく使用しないと恐ろしい副作用があります。
置換で予期しない計算結果になることをマクロの副作用と呼びます。
これの対処法として有効な手段は、マクロの方にかっこをつけておく方法です。

C言語による証明その3(修正版)
#include <stdio.h>

#define A (2 - 0)
#define B (2 - 1)

int main(void) {
   printf("%d", A * B);
   return 0;
}

このようにすれば、正しく動作します。
教訓その3:マクロを使う際は左右にかっこをつけましょう

C言語による証明その4

以下は「1/3が2/3に等しければYESを表示しろ」という趣旨のプログラムである。このプログラムを実行すると、YESを表示する。よって「1/3 = 2/3」であり、両辺に3を掛けて「1 = 2」である。

C言語による証明その4
#include <stdio.h>

int main(void) {
    if(1/3 == 2/3) puts("YES");
    return 0;
}

今回も、コンパイラの警告はありませんでした。

証明その4の答え合わせ

あなたが思っているほどコンピュータは数を表現できない
今回も「コンピュータにおける数の扱い」を正しく理解していないという理由で、このような誤解が生まれています。
「1÷3=0あまり1」で、「2÷3=0あまり2」です。
C99によると、「/演算子の結果は,第 1 オペランドを第 2 オペランドで除した商とし,%演算子の結果は剰余とする。」と書かれているため、
if(1/3 == 2/3)は、
if(0 == 0)となり、等しいので
if(1)となります。
これにより、最初の副文「puts("YES");」が実行され、YESが表示されたというわけです。

証明その4の傾向と対策

証明その4のようなミスを犯す人は、常に値が何の型なのかを意識するべきです。
1int型(整数型)です。
1.0double型(倍精度浮動小数点数型)です。
たったこれだけの違いですが、これで結果は大きく変わってしまうでしょう。

たったこれだけのミスで、500億円を一瞬で失ってしまうこともあります。
1996年6月4日、フランス領ギアナの宇宙センターからヨーロッパ最新の無人サテライト発信ロケット「アリアン5」が打ち上げられました。しかし、打ち上げ後約40秒後に爆発してしまいました。
「アリアン5」には、前世代「アリアン4」で使われていたコードが再利用されていました。
「アリアン5」は「アリアン4」よりも、強力なロケットエンジンを採用したことが引き金となりました。このエラーは、ロケットの水平速度に関連する64ビット浮動小数点を16ビット符号付整数へ変換するときに、加速度が大きすぎたために数字が16ビット符号付整数として保存できる最大値の32,768を超えてしまい、オーバーフローが起こり、最終的には飛行コンピューターがクラッシュしてしまいました。
フライト501では、最初にバックアップ・コンピューターがクラッシュし、それから0.05秒後にメイン・コンピューターがクラッシュしました。その結果、エンジンの出力が過剰になり、ロケットは打ち上げ40秒後に空中分解してしまいました。
これは、歴史上でも最も高くついたバグの1つです。
ロケット本体で500億円、開発費で7000億円もかかっています。
10年かけてつぎ込んだ7500億円の費用を、たったひとつの小さなミスによって、一瞬で失ってしまうのです。

そう、あなたがこのようなミスを犯さないように、あなたは常にどの型を使っているか意識する必要があるのです。
教訓その4:常にあなたが使っている型を意識しよう

C言語による証明その5

最後の証明です。気合を入れていきましょう。

/*= (割って掛けて代入)、*/= (掛けて割って代入) という演算子を用いた方法で証明する。以下のプログラムは、bを2で割ってかけてかけて割った値を表示するものである。

C言語による証明その5
#include <stdio.h>

int main() {
    int a, b;
    a=1; b=1;
    b /*= 2;
    b */= 2;
    printf("a = %d, b = %d", a, b);
    return 0;
}

bは計算上1になるが、ここでは2と表示される。よって1=2となる。

これは!?










よく考えてから、次の節までお進みください。


















では、いいですか?

証明その5の答え合わせ

#include <stdio.h>

int main() {
    int a, b;
    a=1; b=1;
    b /*= 2;
    b */= 2;
    printf("a = %d, b = %d", a, b);
    return 0;
}

証明その5の傾向と対策

説明に騙されたとしても、ソースコードをエディタに貼り付ければ、すぐに気づくはずです。
私が今までアンサイクロペディアから引用したソースコードに、わざわざシンタックスハイライトを使っていなかった理由はこのためです。
なぜなら、色を付けてしまうとバレてしまいますからね。
教訓その5:ソースコードを書いたり、読んだりするときはシンタックスハイライトが使えるエディタを使いましょう

まとめ


ちゃんと警告は読みましょうね。
ちゃんと警告は読みましょうね。(大事なことなので二回言いました)
マクロを使う際は左右にかっこをつけましょう。
常にあなたが使っている型を意識しよう。
ソースコードを書いたり、読んだりするときは、シンタックスハイライトが使えるエディタを使いましょう。

なんだか、想像以上に「C言語(もしくはプログラミング一般)でよくある間違い集」のように思えますね。
まさか、あのアンサイクロペディアから、まともな教訓が得られるとはこれまた恐るべしアンサイクロペディア、って感じです。


  1. 元ネタはStack OverflownのJava ScriptでCan (a ==1 && a== 2 && a==3) ever evaluate to true? 

  2. アンサイクロペディアには「古代C言語」という記事があるから、それに倣っています。決してC言語を侮辱しているわけではありません。