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」と断言してしまうと混乱を招いてしまう気がします。左辺値・右辺値なんて入門者に教える概念でもないから仕方ないのかもしれませんが。

fiore
fiore

自称C++er。

comments powered by Disqus