基底型が bool であるような scoped enum の型変換の異常動作を追い掛けた話

<!-- # 基底型が `bool` であるような scoped enum の型変換の異常動作を追い掛けた話 -->

## 問題

<https://mstdn.anqou.net/@anqou/107190942386550421>

こんな "おもしろコード" を見た。
gcc と clang で挙動が違うらしい。

とりあえず iostream 抜きで再現コードを書いた: <https://wandbox.org/permlink/mxr9FRBUA0nF6rrY>。

```cpp
enum class BoolEnum: bool {};

int main() {
    static_assert(static_cast<bool>(-2), "nonzero should be converted to true");
    static_assert(static_cast<bool>(2), "nonzero should be converted to true");
    static_assert(static_cast<bool>(3), "nonzero should be converted to true");

    constexpr bool indirect = static_cast<bool>(BoolEnum(-2));
    // indirect の値は？
    static_assert(indirect || !indirect, "`indirect` should be true or false");

    static_assert(indirect, "the value is false");
    static_assert(!indirect, "the value is true");

    static_assert(static_cast<bool>(BoolEnum(-3)), "true for any platform");
}
```

scoped enum `BoolEnum` の underlying type として bool を指定した。
`BoolEnum(-2)` を `bool` に変換するとどうなるだろうか？

clang HEAD (13.0.0) でのコンパイル結果 (wandbox.org, 2021-10-31):

```
prog.cc:13:5: error: static_assert failed due to requirement '!indirect' "the value is true"
    static_assert(!indirect, "the value is true");
    ^             ~~~~~~~~~
1 error generated.
```

clang 11.1.0 でのコンパイル結果:

```
prog.cc:12:5: error: static_assert failed due to requirement 'indirect' "the value is false"
    static_assert(indirect, "the value is false");
    ^             ~~~~~~~~
1 error generated.
```

gcc HEAD (12.0.0) (wandbox.org, 2021-10-31) および gcc 11.1.0 でのコンパイル結果:

```
prog.cc: In function 'int main()':
prog.cc:12:19: error: static assertion failed: the value is false
   12 |     static_assert(indirect, "the value is false");
      |                   ^~~~~~~~
```

というわけで、 **clang と gcc のバージョン次第で挙動が異なる** という現象に遭遇した。
clang の HEAD (13.0.0) でのみ `true` で、他では `false` になっている。

さらには、 `BoolEnum(-3)` のように奇数を渡すと clang でも gcc でも `true` になる。

これは未定義動作か、処理系定義の動作か、それともコンパイラのバグだろうか？

まずは規格から追い掛けていこう。
なお、本文書では C++20 規格に editorial fix を適用した post-publication draft である n4868 ([HTML](https://timsong-cpp.github.io/cppwp/n4868/), [PDF](http://open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4868.pdf)) を規格として参照する。

## `static_cast<bool>(-2)` の場合

static cast は [\[expr.static.cast\]](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast) 節で定義されており、特に整数から `bool` 型への変換は [\[expr.static.cast\]/4](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast#4) が該当する。

> An expression `E` can be explicitly converted to a type `T` if there is an implicit conversion sequence (\[over.best.ics\]) from `E` to `T`, (後略)

雑な訳:
> 式 `E` は、 `E` から `T` への暗黙の変換の列 (implicit conversion sequence) がある場合、型 `T` へ明示的に変換 (explicitly convert) することができる。

implicit conversion sequence は複数の変換のうちいずれかである ([\[over.best.ics.general\]/3](https://timsong-cpp.github.io/cppwp/n4868/over.best.ics#general-3))。

> A well-formed implicit conversion sequence is one of the following forms:
> — a standard conversion sequence,
> — a user-defined conversion sequence, or
> — an ellipsis conversion sequence.

雑な訳:
> 合法 (well-formed) な暗黙の変換の列は、以下のいずれかである:
>
> * 標準の変換 (standard conversion) の列、
> * ユーザ定義の変換の列、または
> * ellipsis conversion の列。

今回の場合だと `-2` (整数) から `bool` 型への変換には "standard conversion" が該当し、他の候補は該当しない。

standard conversion の中でも、 `-2` から `bool` 型への変換は [\[conv.bool\]/1](https://timsong-cpp.github.io/cppwp/n4868/conv.bool#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`.

雑な訳:
> 算術型、 unscoped enum 型、ポインタ型、あるいはメンバへのポインタ型の prvalue は、 `bool` の prvalue へと変換できる。
> ゼロ、ヌルポインタ、またはヌルメンバポインタは `false` に変換され、それ以外の値は `true` に変換される。

prvalue については [\[basic.lval\]/1.2](https://timsong-cpp.github.io/cppwp/n4868/basic.lval#1.2) で定義されているが、ここを参照する必要はない。

リテラルについて記述している [\[expr.prim.literal\]/1](https://timsong-cpp.github.io/cppwp/n4868/expr.prim.literal#1) 節で、整数リテラルが prvalue であることが説明されており、整数リテラルは "any other literal" に該当する。

> The type of a literal is determined based on its form as specified in \[lex.literal\].
> A _string-literal_ is an lvalue, a _user-defined-literal_ has the same value category as the corresponding operator call expression described in \[lex.ext\], and any other literal is a prvalue.

雑な訳:
> リテラルの型は \[lex.literal\] 節で指定された形式に基いて決定される。
> _string-literal_ 形式は lvalue、 _user-defined-literal_ 形式は対応する演算子呼出式 (\[lex.ext\] 節で説明されている) と同じ value caterogy、他のあらゆるリテラルは prvalue である。

が、 [\[lex.literal\]](https://timsong-cpp.github.io/cppwp/n4868/lex.literal) 節、特に整数リテラルについて定義している [\[lex.icon\]](https://timsong-cpp.github.io/cppwp/n4868/lex.icon) を確認すると、 `-2` が整数リテラルではないことがわかる。
実のところ `-2` は単項 `-` 演算子を整数値 `2` に適用する式である。
そこで単項 `-` 演算子についても確認すると、 [\[expr.unary.op\]/2](https://timsong-cpp.github.io/cppwp/n4868/expr.unary.op#2) と [\[expr.unary.op\]/8](https://timsong-cpp.github.io/cppwp/n4868/expr.unary.op#8) より単項 `-` 演算子の結果が prvalue であることがわかる。

> The result of each of the following unary operators is a prvalue.
>
> —— \[expr.unary.op\]/2

雑な訳:
> 以下のそれぞれの単項演算子の結果は prvalue である。

<!-- -->

> The operand of the unary `-` operator shall have arithmetic or unscoped enumeration type and the result is the negative of its operand.
>
> —— \[expr.unary.op\]/8

雑な訳:
> 単項 `-` 演算子のオペランドは算術型か unscoped enum 型である必要があり、結果はオペランドを負にしたものである。

以上より prvalue of arithmetic type である `-2` は `bool` へ変換可能で、その結果は "any other value" に相当するため `true` となる。

> 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`.
>
> —— [\[conv.bool\]/1](https://timsong-cpp.github.io/cppwp/n4868/conv.bool#1) (再掲)

## `indirect` の場合

### 整数から enum への型変換

まず `BoolEnum(-2)` について考える。
これは関数スタイルのキャストなので "functional notation" による explicit type conversion に該当する ([\[expr.type.conv\]/1](https://timsong-cpp.github.io/cppwp/n4868/expr.type.conv#1))。

> A _simple-type-specifier_ or _typename-specifier_ followed by a parenthesized optional _expression-list_ or by a _braced-init-list_ (the initializer) constructs a value of the specified type given the initializer. (後略)

雑な訳:
> _simple-type-specifier_ または _typename-specifier_ の後に丸括弧で囲まれた省略可能な _expression-list_ または _braced-init-list_ (初期化子) が来るとき、これは指定された型の値を初期化子を使って構築する。

そして、この式における initializer は parenthesized single expression なので、対応する cast expression と等価である ([\[expr.type.conv\]/2](https://timsong-cpp.github.io/cppwp/n4868/expr.type.conv#2))。

> If the initializer is a parenthesized single expression, the type conversion expression is equivalent to the corresponding cast expression. (後略)

雑な訳:
> 初期化子が丸括弧で囲まれた単一の式であれば、型変換式は対応するキャスト式 (cast expression) と同値である。

cast expression は [\[expr.cast\]](https://timsong-cpp.github.io/cppwp/n4868/expr.cast) 節で定義されており、 functional notation による変換もこれに該当することの明記がある ([\[expr.cast\]/2](https://timsong-cpp.github.io/cppwp/n4868/expr.cast#2))。

> An explicit type conversion can be expressed using functional notation, (中略), or the _cast_ notation.
>
> [\[expr.cast\]/2](https://timsong-cpp.github.io/cppwp/n4868/expr.cast#2)

雑な訳:
> 明示的な型変換は関数形式 (functional notation)、 (中略) またはキャスト形式で表現できる。

cast expression で行われる変換は、 [\[expr.cast\]/4](https://timsong-cpp.github.io/cppwp/n4868/expr.cast#4) で列挙されたもののように解釈可能か順番にテストしていき、最初に解釈に成功したものが使われる。

今回のキャストは最初の候補である [`const_cast`](https://timsong-cpp.github.io/cppwp/n4868/expr.cast#4.1) として解釈可能ではない ([\[expr.const.cast\]](https://timsong-cpp.github.io/cppwp/n4868/expr.const.cast)) ので、ここでは選択されない。

結果としては [`static_cast`](https://timsong-cpp.github.io/cppwp/n4868/expr.cast#4.2) が選ばれるが、以下では解釈が可能であることの確認と同時に挙動も調べていく。

static cast は [\[expr.static.cast\]](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast) 節で定義されており、特に整数から enum 型への変換は [\[expr.static.cast\]/10](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast#10) で説明されている。

> A value of integral or enumeration type can be explicitly converted to a complete enumeration type.
> If the enumeration type has a fixed underlying type, the value is first converted to that type by integral conversion, if necessary, and then to the enumeration type.
> (後略)

雑な訳:
> 整数型か enum 型は完全 (complete) な enum 型へ明示的に変換できる。
> enum 型が固定された (fixed) 基底型 (underlying type) を持つならば、値はまず必要なら基底型へと整数変換 (integral conversion) され、そして enum 型へと変換される。

"a complete enumeration type" とは complete type であるような enum 型のことで、 "complete type" については [\[basic.types.general\]/5](https://timsong-cpp.github.io/cppwp/n4868/basic.types.general#5) で定義されている。

> A class that has been declared but not defined, an enumeration type in certain contexts (\[dcl.enum\]), or an array of unknown bound or of incomplete element type, is an _incompletely-defined_ object type.<sup>43</sup>
> Incompletely-defined object types and _cv_ `void` are _incomplete types_ (\[basic.fundamental\]).

雑な訳:
> 宣言されたが定義されていないクラス、特定の文脈における enum 型、または要素数が不明か不完全 (incomplete) な要素型を持つような配列型は、 不完全に定義された (_incompletely-defined_) オブジェクト型である。
> 不完全に定義されたオブジェクト型と const / volatile で修飾されたものを含む `void` 型は、不完全型 (_incomplete types_) である。

お気持ちとしては、定義やサイズが不明だったりなどして、コンパイラにとって値を作成する方法が未知であるような型が incomplete type であると言われる。
型が incomplete であるかどうかは翻訳単位中で変遷するが、今回考えている例では冒頭で `BoolEnum` 型の定義は完了しており値を作ることができるため、定義より後ろの場所では complete type (もっといえば complete enumeration type) であるといえる。

さて `BoolEnum` は a complete enumeration type で `-2` は integral type の値なので、先に挙げた [\[expr.static.cast\]/10](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast#10) が適用される。

enum 型の値は内部的には整数型として扱われるが、この内部的な型が underlying type であり、これが何らかの既存の別の型であると判明しているような場合に fixed であると呼ばれる ([\[dcl.enum\]/5](https://timsong-cpp.github.io/cppwp/n4868/dcl.enum#5))。
underlying type が明示的に指定されている場合はそれが fixed underlying type であり、あるいは scoped enum で指定がない場合には fixed underlying type として暗黙に int が採用される。

> Each enumeration defines a type that is different from all other types.
> Each enumeration also has an _underlying type_.
> The underlying type can be explicitly specified using an _enum-base_.
> For a scoped enumeration type, the underlying type is `int` if it is not explicitly specified.
> In both of these cases, the underlying type is said to be _fixed_.

雑な訳:
> 各 enum は他のあらゆる型と異なる型を定義する。
> 各 enum は基底型 (_underlying type_) を持つ。
> 基底型は _enum-base_ を使って明示的に指定できる。
> scoped enum 型では、もし明示的に指定されていなければ基底型は `int` になる。
> (明示的に定義されている場合と、 scoped enum で暗黙に `int` にされた場合、) いずれの場合でも、基底型は固定されている (_fixed_) という。

`enum class BoolEnum: bool {};` の場合、 underlying type は `bool` であると明示されているため、 `BoolEnum` は fixed underlying type を持っている。

### 整数から `bool` への型変換

さて、 [\[expr.static.cast\]/10](https://timsong-cpp.github.io/cppwp/n4868/expr.static.cast#10) を再掲する。

> A value of integral or enumeration type can be explicitly converted to a complete enumeration type.
> If the enumeration type has a fixed underlying type, the value is first converted to that type by integral conversion, if necessary, and then to the enumeration type.

雑な訳 (再掲):
> 整数型か enum 型は完全 (complete) な enum 型へ明示的に変換できる。
> enum 型が固定された (fixed) 基底型 (underlying type) を持つならば、値はまず必要なら基底型へと整数変換 (integral conversion) され、そして enum 型へと変換される。

`BoolEnum` は complete enumeration type であり fixed underlying type を持っているため、その値はまず integral conversion によって fixed underlying type へと変換され、しかるのち enumeration type に変換される。

さて問題は underlying type である `bool` への "integral conversion" がどのように行われるかという点である。
`static_cast<bool>(-2)` の場合は implicit conversion sequence が適用される決まりだったため standard conversion に従って非0な値が `true` となったが、今回は standard conversion ではなく integral conversion が名指しで指定されている。

integral conversion は [\[conv.integral\]](https://timsong-cpp.github.io/cppwp/n4868/conv.integral) で定義されており、特に `bool` 型への変換は [\[conv.integral\]/2](https://timsong-cpp.github.io/cppwp/n4868/conv.integral#2) にある。

> If the destination type is `bool`, see \[conv.bool\]. (後略)

雑な訳:
> 変換先の型が `bool` であるなら、 \[conv.bool\] 節を参照せよ。

というわけで、やっぱり integral conversion であっても最終的に非0は `true` になるはずであり、 `static_cast<bool>(BoolEnum(-2))` も `true` にならなければおかしい。

## gcc を追う

こうなってくると、規格に沿っていないと思わしき gcc のソースを追うしかない。
例の挙動 (`static_cast<bool>(BoolEnum)` が `false` になる) は gcc-11.2.0 でも確認できたので、そのソースを追うとしよう。
gcc のソースコードは <https://github.com/gcc-mirror/gcc> にミラーされており、 gcc-11.2.0 のソースは <https://github.com/gcc-mirror/gcc/tree/releases/gcc-11.2.0> である。

なお、以下で抜粋するコードは、引用の際に意味を変えない範囲で暗黙に空白 (インデントや末尾空白の調整) を行うことがあるが、その差異が気になる場合、本物のソースはリンク先等から確認できる。

### scoped enum への型変換

scoped enum は C++ 固有の概念なので、まず C++ 関係のコードがあると思われる `gcc/cp` ディレクトリ以下でそれらしい文字列 ("scoped enum", "dcl.enum", "conv.static", etc.) を検索する。
(ちなみに GitHub から検索すると地獄なのでローカルに clone してきて正規表現などを使うのが良い。)

そこで引っ掛かったのが [gcc/cp/typeck.c の7687〜7715行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/typeck.c#L7687-L7715)である。
以下に[7707〜7715行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/typeck.c#L7707-L7715)を抜粋する。

```c
  if ((INTEGRAL_OR_ENUMERATION_TYPE_P (type)
       || SCALAR_FLOAT_TYPE_P (type))
      && (INTEGRAL_OR_ENUMERATION_TYPE_P (intype)
          || SCALAR_FLOAT_TYPE_P (intype)))
    {
      if (processing_template_decl)
        return expr;
      return ocp_convert (type, expr, CONV_C_CAST, LOOKUP_NORMAL, complain);
    }
```

`type` は変換先の型、 `intype` は変換元の型と思われる。
直前のコメントの内容から考えてもまさにこれが型変換を司っているはずなので、 `ocp_convert` 関数を追う。

`ocp_convert` 関数は `gcc/cp/cvt.c` の689行目から定義されており、 static cast による enum への変換は[806〜837行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L806-L837)が関係していそうだが、ここでは条件次第でエラーや警告が発されているだけで、変換自体は行われていない。
変換は、後続のいくつもの if がスルーされて[883〜886行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L883-L886)で行われているようである。

さて、この部分で特に[821〜836行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L821-L836)に興味深い記述があるため以下に抜粋 (ただし空白を調整した) する。

```c
  /* [expr.static.cast]
     8. A value of integral or enumeration type can be explicitly
     converted to an enumeration type. The value is unchanged if
     the original value is within the range of the enumeration
     values. Otherwise, the resulting enumeration value is
     unspecified.  */
  tree val = fold_for_warn (e);
  if ((complain & tf_warning)
      && TREE_CODE (val) == INTEGER_CST
      && ENUM_UNDERLYING_TYPE (type)
      && !int_fits_type_p (val, ENUM_UNDERLYING_TYPE (type)))
    warning_at (loc, OPT_Wconversion,
                "the result of the conversion is unspecified because "
                "%qE is outside the range of type %qT",
                expr, type);
}
```

前提として[801行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L801)の `if (INTEGRAL_CODE_P (code))` によって変換先の型が整数型であることが確認されており、[806行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L806) の `if (TREE_CODE (type) == ENUMERAL_TYPE)` にて変換先の型が enum 型であることを確認している。

ちなみに `code` 変数は[698行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L698) で `TREE_CODE (type)` として定義されている。
`TREE_CODE` は [gcc のドキュメント](https://gcc.gnu.org/onlinedocs/gccint/Tree-overview.html) によればプログラムの木構造での表現における、ノードの種類 (kind of tree node) のようである。
(なぜ806行目で `code` ではなく `TREE_CODE (type)` としているのかは不明。)

さて、前述の条件に加えて以下の条件も満たすとき、 `-Wconversion` により警告が発されるようになっていそうである。

* 変換元の式が integer constant である
* 変換先の型が underlying type を持つ enum 型である
* 変換元の式が、変換先の enum 型の underlying type に収まるサイズではない

では実際 `-Wconversion` を有効化してコンパイル結果を見てみよう。
ちなみに `-Wconversion` は `-Wall` や `-Wextra` では有効化されない。

```cpp
enum class BoolEnum : bool {};

int main() {
    BoolEnum v = BoolEnum(-2);
    (void)v; // unused variable 警告を抑制する。
}
```

```
prog.cc: In function 'int main()':
prog.cc:4:27: warning: the result of the conversion is unspecified because '-2' is outside the range of type 'BoolEnum' [-Wconversion]
    4 |     BoolEnum v = BoolEnum(-2);
      |                           ^~
```

たしかに注目していた部分のコードのメッセージと一致している。

ちなみにもっと小さそうな型で試しても同様の警告となる。

```cpp
#include <cstdint>

enum class BoolEnum : bool {};

int main() {
    BoolEnum v = BoolEnum(uint8_t{2});
    (void)v;
}
```

```
prog.cc: In function 'int main()':
prog.cc:6:27: warning: the result of the conversion is unspecified because '2' is outside the range of type 'BoolEnum' [-Wconversion]
    6 |     BoolEnum v = BoolEnum(uint8_t{2});
      |                           ^~~~~~~~~~
```

### 整数から整数への型変換

警告をさておいて肝心の変換がどうなっているかというと、先述のとおり[883〜886行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/cvt.c#L883-L886)である。

```c
      converted = convert_to_integer_maybe_fold (type, e, dofold);

      /* Ignore any integer overflow caused by the conversion.  */
      return ignore_overflows (converted, e);
```

整数への変換を行いオーバーフローを無視している、ように見える。
`convert_to_integer_maybe_fold` は [gcc/convert.c の970〜977行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/convert.c#L970-L977)で定義されているため、中身を追う。

```c
tree
convert_to_integer_maybe_fold (tree type, tree expr, bool dofold)
{
  tree result
    = convert_to_integer_1 (type, expr,
                            dofold || CONSTANT_CLASS_OR_WRAPPER_P (expr));
  return preserve_any_location_wrapper (result, expr);
}
```

`convert_to_integer_1` は[同ファイルの448行目から](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/convert.c#L448-L951)定義されている。
変換元の型が整数の場合は[同ファイル651行目から](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/convert.c#L651-L912)のコードが実行されそうである。

今回の変換元の式は `-2` や `2` のような定数なので、[725〜906行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/convert.c#L725-L906)では `dofold` が `true` であったとしても switch の default から break すると思われる。
よって変換は[911行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/convert.c#L911) で起きそうだ。

`fold_convert` は [gcc/fold-const.h の82〜83行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/fold-const.h#L82-L83)で定義されたマクロであり、実際の処理は `fold_convert_loc` 関数で起きている。
`fold_convert_loc` は [gcc/fold-const.c の2386行目から](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/fold-const.c#L2386-L2557)定義されている。

整数定数が boolean type へと変換されているのは[同ファイルの2414-2421行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/fold-const.c#L2414-L2421)の部分である。

```c
    case INTEGER_TYPE: case ENUMERAL_TYPE: case BOOLEAN_TYPE:
    case OFFSET_TYPE:
      if (TREE_CODE (arg) == INTEGER_CST)
        {
          tem = fold_convert_const (NOP_EXPR, type, arg);
          if (tem != NULL_TREE)
            return tem;
        }
```

`arg` は変換元の式で、ここでは `-2` 等の整数定数だったから、 if の中を実行する。
`fold_convert_const` は [gcc/fold-const.c の1989〜2001行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/fold-const.c#L1989-L2001)で定義されている。
短いので全体を抜粋する。

```c
/* A subroutine of fold_convert_const handling conversions of an
   INTEGER_CST to another integer type.  */

static tree
fold_convert_const_int_from_int (tree type, const_tree arg1)
{
  /* Given an integer constant, make new constant with new type,
     appropriately sign-extended or truncated.  Use widest_int
     so that any extension is done according ARG1's type.  */
  return force_fit_type (type, wi::to_widest (arg1),
                         !POINTER_TYPE_P (TREE_TYPE (arg1)),
                         TREE_OVERFLOW (arg1));
}
```

`force_fit_type` は [gcc/tree.c の1440〜1475行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/tree.c#L1440-L1475)で定義されている。
これ以降コードを追い掛けてもあまり情報が増えないので省略するが、この関数は gcc で実装している多倍長整数ライブラリらしきもの (`poly_int`) を使い、与えられた値を変換先の型の精度に変換している。

### scoped enum 型の精度

さて、これまで追い掛けてきた整数から整数への変換だが、変換先の型は実際には `bool` などではなくユーザ定義の `BoolEnum` である。
このコードがうまくいくということは、どこかで `bool` 型の性質が `BoolEnum` へ引き継がれているということである。

これは [gcc/cp/decl.c の15682〜15830行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L15682-L15830)で定義されている `start_enum` 関数で発生する。
(ちなみにこの関数は [gcc/cp/parser.c の20027行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/parser.c#L20027)で呼び出されており、これは `cp_parser_enum_specifier` 関数内なので、たしかにこの関数を追うのが正しそうだと考えられる。)

`underlying_type` で検索していくと、 enum 型に underlying type がありこれが整数型 (`bool` もこれに該当する) である場合、[同ファイル15812行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L15812)で `copy_type_enum(enumtype, underlying_type);` が呼ばれているのがわかる。
`copy_type_enum` 関数は[同ファイルの15639〜15661行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L15639-L15661)で定義されており、特に[15652行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L15652)で精度の設定がされている。
この行で `BoolEnum` の精度が `bool` と同じ精度に設定されるのだろう。

```c
      TYPE_PRECISION (t) = TYPE_PRECISION (src);
```

また、同関数内で最大値、最小値、符号の有無なども設定されている。

### `bool` 型の精度

C++ の bool 型は組み込み型なので、コンパイラ側で精度などの情報を持っている。
こういった組込み型の情報などの初期化は、 C との共通部分は [gcc/c-family/c-common.c の4028行目から](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/c-family/c-common.c#L4028-L4511)定義されている `c_common_nodes_and_builtins` 関数で、 C++ 固有の部分は [gcc/cp/decl.c の4376行目から](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L4376-L4705)定義されている `cxx_init_decl_processing` 関数で行われる。
特に `bool` 型の登録は[4448行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/cp/decl.c#L4448)で行われている。

```c
  record_builtin_type (RID_BOOL, "bool", boolean_type_node);
```

`boolean_type_node` は [gcc/tree.c の10287〜10292行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/tree.c#L10287-L10292) で初期化されており、精度 (precision) が1であることがわかる。

```c
  /* Define a boolean type.  This type only represents boolean values but
     may be larger than char depending on the value of BOOL_TYPE_SIZE.  */
  boolean_type_node = make_unsigned_type (BOOL_TYPE_SIZE);
  TREE_SET_CODE (boolean_type_node, BOOLEAN_TYPE);
  TYPE_PRECISION (boolean_type_node) = 1;
  TYPE_MAX_VALUE (boolean_type_node) = build_int_cst (boolean_type_node, 1);
```

ちなみに `bool` 型自体のサイズは `BOOL_TYPE_SIZE` であり、プラットフォームやモードの都合で再設定されない限り、デフォルトでは1バイトである ([gcc/defaults.h の492〜495行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/defaults.h#L488-L495))。

```c
#ifndef CHAR_TYPE_SIZE
#define CHAR_TYPE_SIZE BITS_PER_UNIT
#endif

#ifndef BOOL_TYPE_SIZE
/* `bool' has size and alignment `1', on almost all platforms.  */
#define BOOL_TYPE_SIZE CHAR_TYPE_SIZE
#endif
```

(これが再設定される例としては [gcc/config/rs6000/darwin.h の448行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/config/rs6000/darwin.h#L448)などがあるが、本題ではないのでここでは抜粋しない。)

### 全体の流れ

以上より、 gcc における該当コードの全体としての処理の流れがわかった。

* パース時
    * scoped enum に整数型としての性質が設定される。
        * `BoolEnum` の場合は `bool` と同じく1ビット精度の整数型となる。
* 型検査時
    * 整数定数から scoped enum への型変換で、 scoped enum の underlying type を整数型であるとして変換を行う。
        * ここでいう整数型とは enum, bool, オフセット型なども含む。
        * このとき **変換先の型が `bool` であっても特別視しない** ([gcc/fold-const.c 2414〜2421行目](https://github.com/gcc-mirror/gcc/blob/releases/gcc-11.2.0/gcc/fold-const.c#L2414-L2421))。
    * `BoolEnum` は1ビット精度整数であるため、1ビット精度整数への縮小が**整数として**行われる。
        * `-2` や `2` は `0` になる。
        * `-1` や `3` は `1` になる。
    * この値が `bool` として解釈され、そのまま `BoolEnum` の値となる。
        * `0` は `false` 、 `1` は `true` である。

## で？

結論: **gcc のバグ**。

OSS でバグを見付けたとなると、報告したくなるものである。
なりますね？？？ (圧)
まずは重複する報告があるか確認してみよう。
enum の static cast まわりのバグなので、キーワードは規格の該当箇所の名前を入れて "expr.static.cast enum" あたりが良いだろうか。

<https://gcc.gnu.org/bugzilla/buglist.cgi?quicksearch=expr.static.cast%20enum>

![screenshot-2021-10-31-055155+0900.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106067/3254a692-8320-4ea9-9982-8c8f67d6215c.png)

ふむ。

[96496 – Conversion to enum with underlying type bool produces wrong result](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=96496)

![screenshot-2021-10-31-055241+0900.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106067/9af9c2f9-3bd4-b208-82c1-1e612bd198f7.png)

そうですね。

![screenshot-2021-10-31-055310+0900.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106067/87d7de7c-d78c-8abf-297b-72a68d2d8f58.png)

![screenshot-2021-10-31-055335+0900.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106067/4140699c-ede9-6870-3900-a55dbad5830b.png)

**おしまい。**

## おまけ: C++17 まで

C++17 の final working draft である n4659 で該当個所 ([\[expr.static.cast\]/10](https://timsong-cpp.github.io/cppwp/n4659/expr.static.cast#10)) を確認すると、 C++20 とは少し文言が異なっている。

> A value of integral or enumeration type can be explicitly converted to a complete enumeration type.
> The value is unchanged if the original value is within the range of the enumeration values (\[dcl.enum\]).
> Otherwise, the behavior is undefined.
> (後略)

雑な訳:

> 整数型または enum 型の値は完全 (complete) な enum 型へ明示的に変換 (explicitly convert) できる。
> 元の値が \[dcl.enum\] で規定される enum の値の範囲に収まっている場合、値は変化しない。
> そうでない場合、未定義動作である。

このように、基底型 (underlying type) の範囲外の値を渡したとき未定義動作ということになっている。
つまりこの記事で解説した事項は C++20 かそれ以降でのみ言えることであって、 C++17 とそれ以前ではそもそも書いてはいけないコードである。

もっとも、未定義ということは何が起きても許されるということだから、 C++20 以降と同様のコンパイル結果となっても当然合法である。
よって、楽をしてコンパイラを実装するなら C++17 あるいはそれ以前でも C++20 以降と同様の挙動をするようになるのではないだろうか。
もちろんコードを書く人はそのような期待をしてはいけないが。

## おまけ: どう対処するか

そもそも C-style の cast を使っているのがよろしくないというのはある。

```cpp
enum class BoolEnum: bool {};

int main() {
    BoolEnum v{2}; // narrowing conversion がエラーになってくれる
    (void)v;
}
```

gcc でのコンパイル結果:

```
prog.cc: In function 'int main()':
prog.cc:4:17: error: narrowing conversion of '2' from 'int' to 'bool' [-Wnarrowing]
    4 |     BoolEnum v{2}; // narrowing conversion がエラーになってくれる
      |                 ^
```

が、とはいえ行儀正しく `static_cast` を使ってしまえばやっぱりバグを踏むことになるし、そもそも用途次第では暗黙に narrowing conversion を引き起こしたいという場合もあろう (特に bool を基底型にするような場合には)。

ひとつの誤魔化し方としては、マクロを使って強制的に `bool` への明示的型変換を経由させる手はある。

```cpp
#define bool_enum(x) static_cast<BoolEnum>(static_cast<bool>(x))
```

これは `BoolEnum{static_cast<bool>(x)}` のようにしても良いのだが、 `new bool_enum(x)` のような妙なことをしたときコンパイルが通るのもなんだか嫌だから、という気分で `static_cast` を使っている。

とはいえこんな気持ち悪い workaround は嫌だというあなたは、 gcc にパッチを投げると大変喜ばれるのではないかと思います。
私はもう疲れたので無理です。