C++のfalseは0でtrueは非0だと思い込んでいた話

目次

WindowsAPIのBOOL型

Windowsプログラミングで使うBOOL型は、minwindef.hで

typedef int BOOL
#define FALSE 0
#define TRUE 1

のように定義されています。FALSE=0、TRUE=1ですから、非常にわかりやすいです(使いやすいかどうかは別にして)。

falseは0でtrueは非0?

では、C++標準のbool型はどうでしょうか。C++を解説しているサイトを見ていると、

  • false = 0、true = 非0
  • false = 0、true = 1

という2通りの定義が見つかります。

私自身は、

  • false = 0
  • true = 非0

だと思っていました。実際、

int getId(const std::string& name); //idを得る関数。エラー時は0が返る

if(getId("Alice")){  // getId()の戻り値が非0、すなわちtrue
  //idが得られたときの処理
}
else{                // getId()の戻り値が0、すなわちfalse
  //idが得られなかったときの処理
}

のような書き方が可能だからです。

ということは、BOOL型のTRUEとは違い、trueが1である保証はないのでしょう。

if(true == 1){
  puts("true == 1");
}

のようなコードは、処理系依存ということですね……本当か?

C++のbool型

bool型とint型の関係

falseとtrueについて、規格書にはちゃんと書いてありました。

A prvalue of type bool can be converted to a prvalue of type int, with false becoming zero and true becoming one.

Working Draft, Standard for Programming Language C++ N4659 7.6.6

拙訳:bool型の純粋右辺値は、int型の純粋右辺値に変換されうる。falseならばint型の0に、trueならばint型の1に変換される。

A prvalue of arithmetic, unscoped enumeration, pointer, or pointer to member type can be converted to a prvalue of type bool. A zero value, null pointer value, or null member pointer value is converted to false; any other value is converted to true. For direct-initialization, a prvalue of type std​::​nullptr_­t can be converted to a prvalue of type bool; the resulting value is false.

Working Draft, Standard for Programming Language C++ N4659 7.14.1

拙訳:算術型、スコープを持たない列挙型(enum class、enum structではなく、enumを使った列挙型)、ポインタ型、メンバへのポインタ型の純粋右辺値は、bool型の純粋右辺値に変換されうる。0、ヌルポインタ、メンバへのヌルポインタは、falseに変換され、他の値はtrueに変換される。直接初期化では、std::nullptr_t型の純粋右辺値は、bool型の純粋右辺値に変換されうる。その場合はfalseに変換される。

(訳は適当なので、間違っていたら指摘していただけると嬉しいです)


純粋右辺値(prvalue・pure rvalue)というのは、

  • リテラル
  • 関数の戻り値(参照を除く)

などを指します。簡単に言うと、「そのものに代入できない式」のことです。

int x = 10; // OK int型変数xは左辺値なので、代入ができる
20 = 10;    // NG 整数リテラル20は純粋右辺値なので、代入ができない
int f() { return -1; }
f() = 10    // NG 関数f()の戻り値は純粋右辺値なので、代入ができない

よくわからないという方は、Qiitaの記事が参考になると思います。

少し話が脱線するのですが、lvalue/rvalueといった言葉を「左辺値」「右辺値」のように訳すのは避けるべきだ、という主張が存在します。

昔のC++では

  • 代入式の左辺にある値だから「左辺値」
  • 代入式の右辺にある値だから「右辺値」

と非常にわかりやすかったのですが、現代のC++においては、この考え方が通用しません。

「左辺値」「右辺値」といった訳は誤解を与えかねないため、そのまま「lvalue」「rvalue」といった言葉を使うべきだ、というのです。

このような考え方には共感しますが、私は

  • lvalue
  • rvalue
  • glvalue
  • xvalue
  • prvalue

といった単語の見た目が似すぎていて、初学者に余計な混乱を与えかねないと考えます。無駄な混乱を避けるために、英語よりはなじみのある日本語を使った言葉のほうが良いでしょう。

そのため、私はlvalueやrvalueといった言葉を使わずに、「左辺値」「右辺値」という言葉を使っています。

……まあ、好きな方を使えば良いと思いますよ。


つまり、他の型からbool型への変換では

  • 0→false
  • 非0→true

なのに対し、bool型からint型への変換では

  • false→0
  • true→1

が保証されているということです。知らなかった。

知らないと起こりそうな事故

とは言っても、「『true == 1』なんて馬鹿げた比較をしなければ問題ないのでは?」とお思いの方もいるかも知れません。しかし、次のコードはどうでしょう。

#include <stdio.h>

// 無名名前空間。中で定義した変数・関数は、このファイル内でしか使えない。(staticのようなもの)
namespace {
  // マスクビット。調べたい変数とのビットアンドを取ることで、フラグが立っているかどうかが調べられる
  constexpr int IS_SUCCESS = 1 << 0; // 0b 0000 0001  0x01
  constexpr int IS_ERROR_1 = 1 << 1; // 0b 0000 0010  0x02
  constexpr int IS_ERROR_2 = 1 << 2; // 0b 0000 0100  0x04
  constexpr int IS_ERROR_3 = 1 << 3; // 0b 0000 1000  0x08
}

int main() {
  // 何らかの状態(関数の戻り値)
  // エラー1とエラー2が起こっているらしい
  int status = 0b00000110;

  if (status & IS_SUCCESS == true) {
    puts("Success");
  }
  if (status & IS_ERROR_1 == true) {
    puts("Error1");
  }
  if (status & IS_ERROR_2 == true) {
    puts("Error2");
  }
  if (status & IS_ERROR_3 == true) {
    puts("Error3");
  }
  return 0;
}

このコードを実行したとき、標準出力には「Error1」と「Error2」が表示されることを期待するでしょう。しかし、このプログラムは何も出力しません。というか、何もしません。一体なぜなのでしょうか。

この部分について考えてみます。

if (status & IS_ERROR_1 == true) {
  puts("Error1");
}

ここを書いた人は、恐らく

if ((status & IS_ERROR_1) == true) {
  puts("Error1");
}

を意図したのだと思います。しかし、ビットアンド「&」は、等価演算子「==」よりも優先順位が低いため、コンパイラは

if (status & (IS_ERROR_1 == true)) {
  puts("Error1");
}

のように解釈します。

ここで、等価演算子「==」は、両辺に純粋右辺値を取ります。ですから、「==」の右辺にある「true」は、純粋右辺値です。

規格書には「bool型の純粋右辺値は、falseなら0、trueなら1に変換される」とあるので、このtrueはint型の1に変換されます。

  • IS_ERROR_1は0b0010、すなわち2
  • trueは1

なのですから、「IS_ERROR_1 == true」の戻り値はfalseになります。

if (status & false) {
  puts("Error1");
}

さらに、ビットアンドの両辺も純粋右辺値を取るので、falseはint型の0に変換されます。

  • statusは0b0110
  • falseは0、すなわち0b0000

なので、「status & false」の戻り値は0b0000、すなわち0になります。

if(0) {
  puts("Error1");
}

また、if文は引数(カッコの中)にbool型の純粋右辺値を取ります。「算術型の純粋右辺値(この場合はint型)はbool型の純粋右辺値に変換される」「0であればfalse、非0であればtrueに変換される」ということなので、0はfalseに変換されます。

if(false) {
  puts("Error1");
}

ということで、if文の中身は絶対に実行されることはないため、コンパイラの最適化によってこのif文は丸々削除されることがわかりました。

// もはや何もない

絶対に実行されることのないif文をどんどん削除していった結果、このプログラム全体のアセンブラは

main:
        xor     eax, eax
        ret

のようになり、何もしないプログラムが完成するというわけです。(今回はstatusが固定なので、Successの部分すら消える)

まあ、このプログラムの場合、コンパイラが警告を出してくれるのですが……

まとめ

1は非0なので、

  • 0 = false
  • 非0 = true

というのも間違いではないんですよね。でも、「0はfalseで非0はtrue」と断言してしまうのもどうなんでしょう。左辺値・右辺値なんて入門者に教える概念でもないから仕方ないのかもしれませんが。

Avatar
fiore

自称C++er。

comments powered by Disqus