最終更新日時(UTC):
が更新

履歴 編集

非推奨だった bool 型に対するインクリメント演算子を削除 [P0002R1](C++17)

このページはC++17に採用された言語機能の変更を解説しています。

のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

概要

C++17ではbool型に対する前置および後置のoperator ++を削除する。

bool型に対する前置および後置のoperator ++とはC++98の時点で非推奨になっていた機能である。

具体的にどのような働きをするのかというと、以下のように値をtrueに書き換える機能をもつ。

#include <iostream>
int main()
{
  bool b = false;
  const bool b1 = ++b;
  std::cout << std::boolalpha << b1 << std::endl; // => true
  const bool b2 = ++b;
  std::cout << std::boolalpha << b2 << std::endl; // => true
}

ここで、前置のoperator ++は、以下のように置き換えられる:

#include <iostream>
int main()
{
  bool b = false;
  b = true;
  std::cout << std::boolalpha << b << std::endl; // => true
  b = true;
  std::cout << std::boolalpha << b << std::endl; // => true
}

一方後置のoperator ++を使う次のようなコードは、以下のようにC++14で標準ライブラリに導入されたstd::exchange()を利用して書き換えることができる。

#include <iostream>

void f(bool b)
{
  std::cout << std::boolalpha << b << std::endl;
}

int main()
{
  bool b = false;
  // 関数fには変数bの現在の値であるfalseの値が渡される
  f(b++); // => false
  std::cout << std::boolalpha << b << std::endl; // => true
}

#include <iostream>
#include <utility>

void f(bool b)
{
  std::cout << std::boolalpha << b << std::endl;
}
int main()
{
  bool b = false;
  f(std::exchange(b, true)); // => false
  std::cout << std::boolalpha << b << std::endl; // => true
}

仕様

これまで、operator ++の定義は、bool型のときはtrueに変更する、operator --の定義はbool型を除く、というように例外規定されていた(§ 8.2.6 expr.post.incr / § 8.3.2 expr.pre.incr)。
C++17ではこれらが削除され、operator ++の定義(§ 8.2.6 expr.post.incr / § 8.3.2 expr.pre.incr)に、bool型を除く、という例外規定が追加された。

前置のoperator ++operator +=の呼び出し(例えば++aa+=1)が等価にならない例に、bool型の場合、という文面があったが、C++17で削除された(§ 8 expr)。

また、組み込みのoperatorのリストのoperator ++に関する文面に、bool型を除く、という例外規定が追加された(§ 16.6 over.built)。

この機能を削除するに至った背景・経緯

この項は十分な出典が存在せず推測でしかないことに注意して読み進めてほしい。

もともとC++の前身であるC言語(ANSI C89)にはbool型は存在しなかった。
そのために、真理値を表すためにbool型の代わりとしてint型やchar型、unsigned char型で代用する例が見られた。

int main(void)
{
  int flag = 0;
  /* do something */
  if(flag){
    /* do something when flag is true*/
  }
  return 0;
}

つまり、非0をtrue、0をfalseとして扱う。

ここで、「初回のみ最大火力で装置をテストし、2回目以降は通常の火力を発射する」シナリオを考えてみよう。

#include <iostream>

void test_firepower() {
  // 最大出力で火力を試す
  std::cout << "最大火力" << std::endl;
}
void fire() {
  // 通常の火力で ”処理” をする
}

int main()
{
  int tested = 0;
  while (true) {
    if (!tested) {
      test_firepower();
    } else {
      fire();
    }
    tested++;
  }
}

これはループのはじめのみtest_firepower関数(最大火力で装置をテスト)が呼び出されることを期待している。しかし期待通りには動かない。
testedint型の最大値になったときif文の条件評価が行われることを考えよう。
testedのインクリメントはオーバーフローするので未定義動作になるが、殆どの環境で2の補数表現を使っているため、int型の最小値になる。
すると、testedの値が再び0まで加算された際に再びtest_firepower関数が呼び出されてしまう

これによく似たバグで少なくとも6つの過度の放射線被曝事故を引き起こし、3人が死亡した例がある。
Therac-25はカナダ原子力公社(AECL)とフランスCGR-MeV社によって開発・製造された放射線療法機器である。
この装置のソフトウェアのバグの一つに、条件変数を非0にする(=trueにする)ために、インクリメントを使っていたというものがあった。
条件変数はC++でいえばstd::uint8_t型で、つまり256回に1度オーバーフローを起こして値が0になるために、falseとして扱われた。
この結果ほかの条件変数の状態によっては25MeVという通常の100倍のβ線が射出されることがあった。

こうした事故を防ぐためなのかは不明だが、C++のbool型はインクリメントした際、常にtrueになるように定められていた。
しかし、そもそも上記のバグを防ぐには、インクリメントではなく単に固定値を代入するようにするべきであり、C++98の時点でdeprecatedになっていたと思われる。

C++14でstd::exchange()が導入されたことにより、唯一使いみちのあった後置のoperator++の必要性もなくなり、C++17で削除されたと推測される。

関連項目

参照