C++ 11では、強く型付けされた列挙型(enum class
)をその基になる型にキャストできます。しかし、同じものへのポインタをキャストすることはできないようです。
enum class MyEnum : int {};
int main()
{
MyEnum me;
int iv = static_cast<int>(me); // works
int* ip = static_cast<int*>(&me); // "invalid static_cast"
}
私はこれがなぜあるべきかを理解しようとしています:これをサポートすることを困難または無意味にする列挙型メカニズムについて何かありますか?それは標準の単純な見落としですか?他に何かありますか?
上記のように列挙型が本当に整数型の上に構築されている場合、値だけでなくポインターもキャストできるはずです。 reinterpret_cast<int*>
またはCスタイルのキャストを引き続き使用できますが、それは私が必要だと思っていたよりも大きなハンマーです。
TL; DR:C++の設計者は、型のパンニングが好きではありません。
他の人は、なぜそれが規格で許可されていないのかを指摘しています。規格の作成者がなぜそのようにしたのかについて説明します。 この提案 によると、強く型付けされた列挙型の主な動機は型の安全性でした。残念ながら、型安全性は多くの人々にとって多くのことを意味します。一貫性が標準化委員会のもう1つの目標であると考えるのは当然なので、C++の他の関連するコンテキストで型安全性を調べてみましょう。
一般に、C++では、(継承によって)関連するように明示的に指定されていない限り、型は関連していません。この例を考えてみましょう。
class A
{
double x;
int y;
};
class B
{
double x;
int y;
};
void foo(A* a)
{
B* b = static_cast<B*>(a); //error
}
AとBの表現はまったく同じですが(標準では「標準レイアウトタイプ」と呼ばれることもあります)、reinterpret_cast
がないと変換できません。同様に、これもエラーです。
class C
{
public:
int x;
};
void foo(C* c)
{
int* intPtr = static_cast<int*>(c); //error
}
Cで唯一のものがintであり、自由にアクセスできることはわかっていますが、static_cast
は失敗します。どうして?これらのタイプが関連していることは明示的に指定されていません。 C++は、オブジェクト指向プログラミングをサポートするように設計されており、構成と継承を区別します。継承によって関連付けられたタイプ間で変換することはできますが、構成によって関連付けられたタイプ間で変換することはできません。
あなたが見た振る舞いに基づいて、強く型付けされた列挙型が構成によってそれらの基礎となる型に関連していることは明らかです。なぜこれが標準委員会が選んだモデルだったのでしょうか?
この問題については、ここに収まるものよりも優れた記事がたくさんありますが、要約してみます。コンポジションを使用する場合と継承を使用する場合は確かに灰色の領域ですが、この場合、コンポジションを支持する多くのポイントがあります。
この場合、継承または構成が優れているかどうかについて何日も議論することができますが、最終的には決定を下す必要があり、動作は構成に基づいてモデル化されました。
代わりに、少し異なる方法でそれを見てください。 int
とlong
の基底表現が同一であっても、static_cast
a long*
からint*
への変換はできません。同じ理由で、int
に基づく列挙型は、まだint
とは無関係の一意の型として扱われるため、reinterpret_cast
が必要です。
列挙型は、名前付き定数を持つ別個の型(3.9.2)です。 [...]各列挙型は、他のすべての型とは異なる型を定義します。 [...] 2つの列挙型は、基になる型が同じである場合、レイアウト互換です。
[dcl.enum](§7.2)
基になる型は、型システム内の他の型との関係ではなく、メモリ内の列挙型のレイアウトを指定します(標準で言うように、それは個別の型であり、独自の型です)。 enum : int {}
へのポインタが暗黙的にint*
に変換されることはありません。これは、struct { int i; };
へのポインタがすべてメモリ内で同じように見えても、変換できないのと同じ方法です。
では、そもそもなぜint
への暗黙の変換が機能するのでしょうか。
基になる型が固定されている列挙型の場合、列挙型の値は基になる型の値です。 [...]列挙型またはスコープ外の列挙型のオブジェクトの値は、汎整数拡張(4.5)によって整数に変換されます。
[dcl.enum](§7.2)
したがって、列挙型の値はint
型であるため、int
に割り当てることができます。整数型の昇格の規則により、列挙型のオブジェクトをint
に割り当てることができます。ちなみに、ここでの標準は、これがCスタイル(スコープなし)の列挙型にのみ当てはまると具体的に指摘しています。これは、例の最初の行にstatic_cast<int>
がまだ必要であることを意味しますが、enum class : int
をenum : int
に変えるとすぐに、明示的なキャストなしで機能します。しかし、ポインタ型にはまだ運がありません。
インテグラルプロモーションは、標準の[conv.prom](§4.5)で定義されています。セクション全体の引用の詳細は割愛しますが、ここで重要な詳細は、そこにあるすべてのルールが非ポインタ型のprvaluesに適用されるため、これは適用されないということです。私たちの小さな問題に。
パズルの最後のピースは[expr.static.cast](§5.2.9)にあり、static_cast
の方法を説明しています。動作します。
スコープ付き列挙型(7.2)の値は、明示的に整数型に変換できます。
これが、enum class
からint
へのキャストが機能する理由です。
ただし、ポインタ型で許可されているすべてのstatic_cast
s(ここでも、かなり長いセクションは引用しません)には、型間の何らかの関係が必要であることに注意してください。答えの始まりを覚えている場合、各列挙型は別個の型であるため、それらの基になる型または同じ基になる型の他の列挙型との関係はありません。
これは @ MarkBの答え :静的-ポインタenum
をint
へのポインタにキャストすることは、ある整数型から別の整数型にポインタをキャストすることに似ています。両方の下に同じメモリレイアウトがあり、一方の値がルールの整数プロモーションによって暗黙的に他方に変換される場合、それらはまだ無関係なタイプであるため、static_cast
はここでは機能しません。
思考の誤りはそれだと思います
enum class MyEnum : int {};
本当に継承ではありません。もちろん、MyEnum
is anint
と言うことができます。ただし、int
sで使用できるすべての操作がMyEnum
でも使用できるわけではないため、従来の継承とは異なります。
これを次のものと比較してみましょう。円は楕円です。ただし、楕円で可能なすべての操作が円でも可能であるとは限らないため、CirlceShape
をEllipseShape
から継承するものとして実装することはほとんどの場合間違っています。簡単な例は、形状をx方向にスケーリングすることです。
したがって、列挙型クラスを整数型から継承と考えると、混乱が生じます。列挙型クラスのインスタンスをインクリメントすることはできませんが、整数をインクリメントすることはできます。 本当に継承ではないので、これらの型へのポインタを静的にキャストすることを禁止することは理にかなっています。次の行は安全ではありません。
++*reinterpret_cast<int*>(&me);
この場合、委員会がstatic_cast
を禁止したのはこのためかもしれません。一般に、reinterpret_cast
は悪であると見なされ、static_cast
は問題ないと見なされます。
あなたの質問への答えは、ドラフト標準のセクション5.2.9静的キャストにあります。
許可のサポート
int iv = static_cast<int>(me);
以下から入手できます。
5.2.9/9スコープ付き列挙型(7.2)の値は、明示的に整数型に変換できます。元の値を指定されたタイプで表すことができる場合、値は変更されません。それ以外の場合、結果の値は指定されません。
許可のサポート
me = static_cast<MyEnum>(100);
以下から入手できます。
5.2.9/10整数型または列挙型の値は、明示的に列挙型に変換できます。元の値が列挙値(7.2)の範囲内にある場合、値は変更されません。それ以外の場合、結果の値は指定されません(そして、その範囲内にない可能性があります)。
許可しないためのサポート
int* ip = static_cast<int*>(&me);
以下から入手できます。
5.2.9/11タイプ「pointertocv1 B」のprvalue(Bはクラスタイプ)は、タイプ「pointer to cv2 D」のprvalueに変換できます。ここで、DはBから派生したクラス(条項10)です。 、「pointertoD」から「pointertoB」への有効な標準変換が存在する場合(4.10)、cv2はcv1と同じかそれ以上のcv-qualificationであり、Bはの仮想基本クラスでもありません。 DまたはDの仮想基本クラスの基本クラス。nullポインター値(4.10)は、宛先タイプのnullポインター値に変換されます。タイプ「pointertocv1 B」のprvalueが実際にはタイプDのオブジェクトのサブオブジェクトであるBを指している場合、結果のポインターはタイプDの囲んでいるオブジェクトを指します。それ以外の場合、キャストの結果は未定義です。
static_cast
をキャストに使用することはできません&me
からint*
MyEnum
とint
は継承によって関連付けられていないため。
最初のstatic_cast
の理由は、古いスタイルのenum
を期待する関数やライブラリを操作できること、または列挙に定義された値の束を使用して、整数型を直接期待できることだと思います。ただし、型enum
と整数型の間には他の論理関係がないため、そのキャストが必要な場合はreinterpret_cast
を使用する必要があります。ただし、reinterpret_cast
に問題がある場合は、独自のヘルパーを使用できます。
template< class EnumT >
typename std::enable_if<
std::is_enum<EnumT>::value,
typename std::underlying_type<EnumT>::type*
>::type enum_as_pointer(EnumT& e)
{
return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}
または
template< class IntT, class EnumT >
IntT* static_enum_cast(EnumT* e,
typename std::enable_if<
std::is_enum<EnumT>::value &&
std::is_convertible<
typename std::underlying_type<EnumT>::type*,
IntT*
>::value
>::type** = nullptr)
{
return reinterpret_cast<typename std::underlying_type<EnumT>::type*>(&e);
}
この回答はreason of prohibiting static_cast of enum pointers
について満足できないかもしれませんが、reinterpret_cast
を安全に使用する方法を提供します。