web-dev-qa-db-ja.com

C ++ 11の強く型付けされた列挙型をポインターを介して基になる型にキャストできないのはなぜですか?

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スタイルのキャストを引き続き使用できますが、それは私が必要だと思っていたよりも大きなハンマーです。

24
John Zwinck

TL; DR:C++の設計者は、型のパンニングが好きではありません。

他の人は、なぜそれが規格で許可されていないのかを指摘しています。規格の作成者がなぜそのようにしたのかについて説明します。 この提案 によると、強く型付けされた列挙型の主な動機は型の安全性でした。残念ながら、型安全性は多くの人々にとって多くのことを意味します。一貫性が標準化委員会のもう1つの目標であると考えるのは当然なので、C++の他の関連するコンテキストで型安全性を調べてみましょう。

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++は、オブジェクト指向プログラミングをサポートするように設計されており、構成と継承を区別します。継承によって関連付けられたタイプ間で変換することはできますが、構成によって関連付けられたタイプ間で変換することはできません。

あなたが見た振る舞いに基づいて、強く型付けされた列挙型が構成によってそれらの基礎となる型に関連していることは明らかです。なぜこれが標準委員会が選んだモデルだったのでしょうか?

構成と継承

この問題については、ここに収まるものよりも優れた記事がたくさんありますが、要約してみます。コンポジションを使用する場合と継承を使用する場合は確かに灰色の領域ですが、この場合、コンポジションを支持する多くのポイントがあります。

  1. 強く型付けされた列挙型は、整数値として使用されることを意図していません。したがって、継承によって示される「is-a」関係は適合しません。
  2. 最高レベルでは、列挙型は離散値のセットを表すことを意味します。これが各値にID番号を割り当てることによって実装されるという事実は、一般的に重要ではありません(残念ながら、Cはこの関係を公開し、強制します)。
  3. 提案 を振り返ると、指定された基になる型を許可する理由としてリストされているのは、列挙型のサイズと符号を指定するためです。これは、列挙型の本質的な部分というよりも実装の詳細であり、やはり構成を支持します。

この場合、継承または構成が優れているかどうかについて何日も議論することができますが、最終的には決定を下す必要があり、動作は構成に基づいてモデル化されました。

13
Falias

代わりに、少し異なる方法でそれを見てください。 intlongの基底表現が同一であっても、static_cast a long*からint*への変換はできません。同じ理由で、intに基づく列挙型は、まだintとは無関係の一意の型として扱われるため、reinterpret_castが必要です。

8
Mark B

列挙型は、名前付き定数を持つ別個の型(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 : intenum : intに変えるとすぐに、明示的なキャストなしで機能します。しかし、ポインタ型にはまだ運がありません。

インテグラルプロモーションは、標準の[conv.prom](§4.5)で定義されています。セクション全体の引用の詳細は割愛しますが、ここで重要な詳細は、そこにあるすべてのルールが非ポインタ型のprvaluesに適用されるため、これは適用されないということです。私たちの小さな問題に。

パズルの最後のピースは[expr.static.cast](§5.2.9)にあり、static_castの方法を説明しています。動作します。

スコープ付き列挙型(7.2)の値は、明示的に整数型に変換できます。

これが、enum classからintへのキャストが機能する理由です。

ただし、ポインタ型で許可されているすべてのstatic_casts(ここでも、かなり長いセクションは引用しません)には、型間の何らかの関係が必要であることに注意してください。答えの始まりを覚えている場合、各列挙型は別個の型であるため、それらの基になる型または同じ基になる型の他の列挙型との関係はありません。

これは @ MarkBの答え :静的-ポインタenumintへのポインタにキャストすることは、ある整数型から別の整数型にポインタをキャストすることに似ています。両方の下に同じメモリレイアウトがあり、一方の値がルールの整数プロモーションによって暗黙的に他方に変換される場合、それらはまだ無関係なタイプであるため、static_castはここでは機能しません。

7
ComicSansMS

思考の誤りはそれだと思います

enum class MyEnum : int {};

本当に継承ではありません。もちろん、MyEnumis anintと言うことができます。ただし、intsで使用できるすべての操作がMyEnumでも使用できるわけではないため、従来の継承とは異なります。

これを次のものと比較してみましょう。円は楕円です。ただし、楕円で可能なすべての操作が円でも可能であるとは限らないため、CirlceShapeEllipseShapeから継承するものとして実装することはほとんどの場合間違っています。簡単な例は、形状をx方向にスケーリングすることです。

したがって、列挙型クラスを整数型から継承と考えると、混乱が生じます。列挙型クラスのインスタンスをインクリメントすることはできませんが、整数をインクリメントすることはできます。 本当に継承ではないので、これらの型へのポインタを静的にキャストすることを禁止することは理にかなっています。次の行は安全ではありません。

++*reinterpret_cast<int*>(&me);

この場合、委員会がstatic_castを禁止したのはこのためかもしれません。一般に、reinterpret_castは悪であると見なされ、static_castは問題ないと見なされます。

6
Ralph Tandetzky

あなたの質問への答えは、ドラフト標準のセクション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*MyEnumintは継承によって関連付けられていないため。

3
R Sahu

最初の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を安全に使用する方法を提供します。

1
BigBoss