問題
こんな "おもしろコード" を見た。
gcc と clang で挙動が違うらしい。
とりあえず iostream 抜きで再現コードを書いた: https://wandbox.org/permlink/mxr9FRBUA0nF6rrY。
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, PDF) を規格として参照する。
static_cast<bool>(-2)
の場合
static cast は [expr.static.cast] 節で定義されており、特に整数から bool
型への変換は [expr.static.cast]/4 が該当する。
An expression
E
can be explicitly converted to a typeT
if there is an implicit conversion sequence ([over.best.ics]) fromE
toT
, (後略)
雑な訳:
式
E
は、E
からT
への暗黙の変換の列 (implicit conversion sequence) がある場合、型T
へ明示的に変換 (explicitly convert) することができる。
implicit conversion sequence は複数の変換のうちいずれかである ([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 で定義されている。
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 tofalse
; any other value is converted totrue
.
雑な訳:
算術型、 unscoped enum 型、ポインタ型、あるいはメンバへのポインタ型の prvalue は、
bool
の prvalue へと変換できる。
ゼロ、ヌルポインタ、またはヌルメンバポインタはfalse
に変換され、それ以外の値はtrue
に変換される。
prvalue については [basic.lval]/1.2 で定義されているが、ここを参照する必要はない。
リテラルについて記述している [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] 節、特に整数リテラルについて定義している [lex.icon] を確認すると、 -2
が整数リテラルではないことがわかる。
実のところ -2
は単項 -
演算子を整数値 2
に適用する式である。
そこで単項 -
演算子についても確認すると、 [expr.unary.op]/2 と [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 tofalse
; any other value is converted totrue
.—— [conv.bool]/1 (再掲)
indirect
の場合
整数から enum への型変換
まず BoolEnum(-2)
について考える。
これは関数スタイルのキャストなので "functional notation" による explicit type conversion に該当する ([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)。
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] 節で定義されており、 functional notation による変換もこれに該当することの明記がある ([expr.cast]/2)。
An explicit type conversion can be expressed using functional notation, (中略), or the cast notation.
雑な訳:
明示的な型変換は関数形式 (functional notation)、 (中略) またはキャスト形式で表現できる。
cast expression で行われる変換は、 [expr.cast]/4 で列挙されたもののように解釈可能か順番にテストしていき、最初に解釈に成功したものが使われる。
今回のキャストは最初の候補である const_cast
として解釈可能ではない ([expr.const.cast]) ので、ここでは選択されない。
結果としては static_cast
が選ばれるが、以下では解釈が可能であることの確認と同時に挙動も調べていく。
static cast は [expr.static.cast] 節で定義されており、特に整数から enum 型への変換は [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 で定義されている。
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.43
Incompletely-defined object types and cvvoid
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 が適用される。
enum 型の値は内部的には整数型として扱われるが、この内部的な型が underlying type であり、これが何らかの既存の別の型であると判明しているような場合に fixed であると呼ばれる ([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 isint
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 を再掲する。
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] で定義されており、特に bool
型への変換は [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行目である。
以下に7707〜7715行目を抜粋する。
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行目が関係していそうだが、ここでは条件次第でエラーや警告が発されているだけで、変換自体は行われていない。
変換は、後続のいくつもの if がスルーされて883〜886行目で行われているようである。
さて、この部分で特に821〜836行目に興味深い記述があるため以下に抜粋 (ただし空白を調整した) する。
/* [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行目の if (INTEGRAL_CODE_P (code))
によって変換先の型が整数型であることが確認されており、806行目 の if (TREE_CODE (type) == ENUMERAL_TYPE)
にて変換先の型が enum 型であることを確認している。
ちなみに code
変数は698行目 で TREE_CODE (type)
として定義されている。
TREE_CODE
は gcc のドキュメント によればプログラムの木構造での表現における、ノードの種類 (kind of tree node) のようである。
(なぜ806行目で code
ではなく TREE_CODE (type)
としているのかは不明。)
さて、前述の条件に加えて以下の条件も満たすとき、 -Wconversion
により警告が発されるようになっていそうである。
- 変換元の式が integer constant である
- 変換先の型が underlying type を持つ enum 型である
- 変換元の式が、変換先の enum 型の underlying type に収まるサイズではない
では実際 -Wconversion
を有効化してコンパイル結果を見てみよう。
ちなみに -Wconversion
は -Wall
や -Wextra
では有効化されない。
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);
| ^~
たしかに注目していた部分のコードのメッセージと一致している。
ちなみにもっと小さそうな型で試しても同様の警告となる。
#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行目である。
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行目で定義されているため、中身を追う。
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行目から定義されている。
変換元の型が整数の場合は同ファイル651行目からのコードが実行されそうである。
今回の変換元の式は -2
や 2
のような定数なので、725〜906行目では dofold
が true
であったとしても switch の default から break すると思われる。
よって変換は911行目 で起きそうだ。
fold_convert
は gcc/fold-const.h の82〜83行目で定義されたマクロであり、実際の処理は fold_convert_loc
関数で起きている。
fold_convert_loc
は gcc/fold-const.c の2386行目から定義されている。
整数定数が boolean type へと変換されているのは同ファイルの2414-2421行目の部分である。
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行目で定義されている。
短いので全体を抜粋する。
/* 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行目で定義されている。
これ以降コードを追い掛けてもあまり情報が増えないので省略するが、この関数は gcc で実装している多倍長整数ライブラリらしきもの (poly_int
) を使い、与えられた値を変換先の型の精度に変換している。
scoped enum 型の精度
さて、これまで追い掛けてきた整数から整数への変換だが、変換先の型は実際には bool
などではなくユーザ定義の BoolEnum
である。
このコードがうまくいくということは、どこかで bool
型の性質が BoolEnum
へ引き継がれているということである。
これは gcc/cp/decl.c の15682〜15830行目で定義されている start_enum
関数で発生する。
(ちなみにこの関数は gcc/cp/parser.c の20027行目で呼び出されており、これは cp_parser_enum_specifier
関数内なので、たしかにこの関数を追うのが正しそうだと考えられる。)
underlying_type
で検索していくと、 enum 型に underlying type がありこれが整数型 (bool
もこれに該当する) である場合、同ファイル15812行目で copy_type_enum(enumtype, underlying_type);
が呼ばれているのがわかる。
copy_type_enum
関数は同ファイルの15639〜15661行目で定義されており、特に15652行目で精度の設定がされている。
この行で BoolEnum
の精度が bool
と同じ精度に設定されるのだろう。
TYPE_PRECISION (t) = TYPE_PRECISION (src);
また、同関数内で最大値、最小値、符号の有無なども設定されている。
bool
型の精度
C++ の bool 型は組み込み型なので、コンパイラ側で精度などの情報を持っている。
こういった組込み型の情報などの初期化は、 C との共通部分は gcc/c-family/c-common.c の4028行目から定義されている c_common_nodes_and_builtins
関数で、 C++ 固有の部分は gcc/cp/decl.c の4376行目から定義されている cxx_init_decl_processing
関数で行われる。
特に bool
型の登録は4448行目で行われている。
record_builtin_type (RID_BOOL, "bool", boolean_type_node);
boolean_type_node
は gcc/tree.c の10287〜10292行目 で初期化されており、精度 (precision) が1であることがわかる。
/* 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行目)。
#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行目などがあるが、本題ではないのでここでは抜粋しない。)
全体の流れ
以上より、 gcc における該当コードの全体としての処理の流れがわかった。
- パース時
- scoped enum に整数型としての性質が設定される。
-
BoolEnum
の場合はbool
と同じく1ビット精度の整数型となる。
-
- scoped enum に整数型としての性質が設定される。
- 型検査時
- 整数定数から scoped enum への型変換で、 scoped enum の underlying type を整数型であるとして変換を行う。
- ここでいう整数型とは enum, bool, オフセット型なども含む。
- このとき 変換先の型が
bool
であっても特別視しない (gcc/fold-const.c 2414〜2421行目)。
-
BoolEnum
は1ビット精度整数であるため、1ビット精度整数への縮小が整数として行われる。-
-2
や2
は0
になる。 -
-1
や3
は1
になる。
-
- この値が
bool
として解釈され、そのままBoolEnum
の値となる。-
0
はfalse
、1
はtrue
である。
-
- 整数定数から scoped enum への型変換で、 scoped enum の underlying type を整数型であるとして変換を行う。
で?
結論: gcc のバグ。
OSS でバグを見付けたとなると、報告したくなるものである。
なりますね??? (圧)
まずは重複する報告があるか確認してみよう。
enum の static cast まわりのバグなので、キーワードは規格の該当箇所の名前を入れて "expr.static.cast enum" あたりが良いだろうか。
ふむ。
96496 – Conversion to enum with underlying type bool produces wrong result
そうですね。
おしまい。
おまけ: C++17 まで
C++17 の final working draft である 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 を使っているのがよろしくないというのはある。
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
への明示的型変換を経由させる手はある。
#define bool_enum(x) static_cast<BoolEnum>(static_cast<bool>(x))
これは BoolEnum{static_cast<bool>(x)}
のようにしても良いのだが、 new bool_enum(x)
のような妙なことをしたときコンパイルが通るのもなんだか嫌だから、という気分で static_cast
を使っている。
とはいえこんな気持ち悪い workaround は嫌だというあなたは、 gcc にパッチを投げると大変喜ばれるのではないかと思います。
私はもう疲れたので無理です。