web-dev-qa-db-ja.com

C ++での列挙の前方宣言

私は次のようなことをしようとしています:

enum E;

void Foo(E e);

enum E {A, B, C};

コンパイラはそれを拒否します。私はグーグルをざっと見てきましたが、コンセンサスは「できません」と思われますが、その理由は理解できません。誰でも説明できますか?

明確化2:クラスにプライベートメソッドがあり、上記の列挙型を取得するため、列挙型の値を公開したくないため、これを実行しています。たとえば、Eが

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

プロジェクトXは、ユーザーに知ってもらいたいものではありません。

そこで、プライベートメソッドをヘッダーファイルに配置し、enumをcppで内部的に宣言し、ビルドされたライブラリファイルとヘッダーを人々に配布できるように、enumを前方宣言したいと思いました。

コンパイラについては-GCCです。

248
szevvy

列挙型を前方宣言できない理由は、値を知らないと、コンパイラーが列挙型変数に必要なストレージを知ることができないためです。 C++コンパイラは、指定されたすべての値を含めるために必要なサイズに基づいて、実際のストレージスペースを指定できます。表示されるのが前方宣言だけである場合、変換ユニットはどのストレージサイズが選択されるかを知ることができません-それはcharまたはint、または何か他のものである可能性があります。


ISO C++標準のセクション7.2.5から:

列挙の基になる型は、列挙で定義されたすべての列挙子値を表すことができる整数型です。列挙型の値がintまたはunsigned intに収まらない場合を除き、基になる型がintより大きくなってはならないことを除いて、列挙の基になる型として使用される整数型は実装定義です。 enumerator-listが空の場合、基になる型は、列挙に値0の単一の列挙子が含まれているかのようになります。列挙に適用されるsizeof()の値type、列挙型のオブジェクト、または列挙子は、基になる型に適用されるsizeof()の値です。

関数のcallerは、呼び出しスタックを正しくセットアップするためにパラメーターのサイズを知っている必要があるため、列挙リスト内の列挙の数を事前に知っておく必要があります関数のプロトタイプ。

更新:C++ 0Xでは、enum型を事前に宣言するための構文が提案され、受け入れられました。提案は http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf で見ることができます

201
KJAWolf

C++ 0xでは、列挙型の前方宣言も可能です。以前は、列挙型が前方宣言できない理由は、列挙のサイズがその内容に依存するためです。列挙のサイズがアプリケーションによって指定されている限り、前方宣言できます。

enum Enum1;                   //Illegal in C++ and C++0x; no size is explicitly specified.
enum Enum2 : unsigned int;    //Legal in C++0x.
enum class Enum3;             //Legal in C++0x, because enum class declarations have a default type of "int".
enum class Enum4: unsigned int; //Legal C++0x.
enum Enum2 : unsigned short;  //Illegal in C++0x, because Enum2 was previously declared with a different type.
187
user119017

最近の開発状況を踏まえて、ここに最新の回答を追加します。

C++ 11では、ストレージタイプを同時に宣言する限り、enumを前方宣言できます。構文は次のようになります。

enum E : short;
void foo(E e);

....

enum E : short
{
    VALUE_1,
    VALUE_2,
    ....
}

実際、関数が列挙の値を参照しない場合、その時点で完全な宣言はまったく必要ありません。

これはG ++ 4.6以降でサポートされています(最近のバージョンでは-std=c++0xまたは-std=c++11)。 Visual C++ 2013はこれをサポートしています。以前のバージョンでは、まだわからなかった非標準のサポートがあります-単純な前方宣言は合法ですが、YMMVであるという提案がありました。

72
Tom

C++での前方宣言は、 コンパイル時間を劇的に高速化する であるため非常に便利です。 C++では、structclassfunctionなど、いくつかのことを前方宣言できます。

しかし、C++でenumを前方宣言できますか?

いいえ、できません。

しかし、なぜそれを許可しないのですか?許可されている場合は、ヘッダーファイルでenum型を定義し、ソースファイルでenum値を定義できます。それは正しいように思える?

違う。

C++には、C#(int)にあるようなenumのデフォルトタイプはありません。 C++では、enum型は、コンパイラによって、enumの値の範囲に適合する任意の型に決定されます。

どういう意味ですか?

enumのすべての値を定義するまで、enumの基になる型を完全に決定することはできません。 enumの宣言と定義を区別できないのはどちらですか。したがって、C++でenumを前方宣言することはできません。

ISO C++標準S7.2.5:

列挙の基になる型は、列挙で定義されたすべての列挙子値を表すことができる整数型です。列挙型の値がintまたはunsigned intに収まらない場合を除き、基になる型がintより大きくなってはならないことを除いて、列挙の基になる型として使用される整数型は実装定義です。列挙子リストが空の場合、基になる型は、列挙に値0の単一の列挙子があるかのようになります。列挙型、列挙型のオブジェクト、または列挙子に適用されるsizeof()の値は、適用されるsizeof()の値です基になる型に。

sizeof演算子を使用して、C++の列挙型のサイズを決定できます。列挙型のサイズは、基になる型のサイズです。このようにして、コンパイラがenumに使用しているタイプを推測できます。

enumの型を次のように明示的に指定するとどうなりますか:

enum Color : char { Red=0, Green=1, Blue=2};
assert(sizeof Color == 1);

その後、enumを前方宣言できますか?

いいえ。でも、なぜですか?

enumの型の指定は、実際には現在のC++標準の一部ではありません。これはVC++拡張機能です。ただし、C++ 0xの一部になります。

ソース

30
Brian R. Bondy

[私の答えは間違っていますが、コメントが役立つのでここに残しました]。

異なる列挙型へのポインタが同じサイズであるとは限らないため、前方宣言列挙型は非標準です。コンパイラは、このタイプで使用できるサイズポインターを知るために、定義を確認する必要がある場合があります。

実際には、少なくともすべての一般的なコンパイラでは、列挙型へのポインタは一貫したサイズです。列挙型の前方宣言は、たとえばVisual C++によって言語拡張として提供されます。

13
James Hopkin

実際、enumの前方宣言のようなものはありません。列挙型の定義には、その列挙型を使用する他のコードに依存する可能性のあるコードが含まれていないため、最初に宣言するときに列挙型を完全に定義することは通常問題になりません。

列挙型の唯一の用途がプライベートメンバー関数による場合、列挙自体をそのクラスのプライベートメンバーとして持つことにより、カプセル化を実装できます。列挙は、宣言の時点、つまりクラス定義内で完全に定義する必要があります。ただし、プライベートメンバー関数がそこで機能することを宣言しているので、これは大きな問題ではなく、実装内部のそれ以上の悪化もありません。

実装の詳細にさらに深い隠蔽が必要な場合は、純粋な仮想関数のみで構成される抽象インターフェースと、インターフェースを実装(継承)する具体的で完全に隠されたクラスに分割できます。クラスインスタンスの作成は、ファクトリまたはインターフェイスの静的メンバー関数によって処理できます。そうすれば、プライベート関数はもちろんのこと、実際のクラス名も公開されません。

7

その理由が実際にはisであることに注意してください。enumのサイズは、前方宣言後はまだ不明です。まあ、構造体の前方宣言を使用して、前方宣言された構造体定義自体で参照されている場所からポインタを渡したり、オブジェクトを参照したりすることができます。

列挙型を値ごとに渡すことを望むため、列挙型を前方宣言することはあまり有用ではありません。いくつかのプラットフォームではintやlongとは異なるサイズのポインターを使用するように言われたので、あなたはそれへのポインターさえ持つことができませんでした。したがって、すべて列挙型の内容に依存します。

現在のC++標準では、次のようなことを明示的に禁止しています。

enum X;

7.1.5.3/1内)。しかし、来年に予定されている次のC++標準では、次のことが許可されているため、実際に問題がhasであると確信しました。

enum X : int;

これは「不透明な」列挙型宣言として知られています。次のコードでX 値によるを使用することもできます。そして、その列挙子は、後で列挙の再宣言で定義できます。現在の作業ドラフトの7.2を参照してください。

私はこのようにします:

[公開ヘッダー内]

typedef unsigned long E;

void Foo(E e);

[内部ヘッダー内]

enum Econtent { FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X,
  FORCE_32BIT = 0xFFFFFFFF };

FORCE_32BITを追加することにより、Econtentが確実に長いものにコンパイルされるため、Eと互換性があります。

4
Laurie Cheers

GCCでは前方宣言できないようです!

興味深い議論 ここ

2
prakash

列挙型を構造体にラップして、いくつかのコンストラクターと型変換を追加し、代わりに構造体を前方宣言できます。

#define ENUM_CLASS(NAME, TYPE, VALUES...) \
struct NAME { \
    enum e { VALUES }; \
    explicit NAME(TYPE v) : val(v) {} \
    NAME(e v) : val(v) {} \
    operator e() const { return e(val); } \
    private:\
        TYPE val; \
}

これは動作しているようです: http://ideone.com/TYtP2

2
Leszek Swirski

ヘッダーファイルに列挙型を表示したくない場合、プライベートメソッドでのみ使用するようにする場合は、pimplの原則に従うことで解決できます。

これは、宣言するだけでヘッダーのクラス内部を非表示にする手法です。

class A 
{
public:
    ...
private:
    void* pImpl;
};

次に、実装ファイル(cpp)で、内部の表現となるクラスを宣言します。

class AImpl
{
public:
    AImpl(A* pThis): m_pThis(pThis) {}

    ... all private methods here ...
private:
    A* m_pThis;
};

クラスコンストラクターで実装を動的に作成し、デストラクタで削除する必要があります。パブリックメソッドを実装するときは、次を使用する必要があります。

((AImpl*)pImpl)->PrivateMethod();

Pimplを使用することには長所があります。1つは、クラスヘッダーを実装から切り離すことです。1つのクラス実装を変更するときに他のクラスを再コンパイルする必要はありません。もう1つは、ヘッダーが非常に単純であるため、コンパイル時間を短縮できることです。

しかし、使用するのは苦痛なので、ヘッダーで列挙型をプライベートとして宣言するだけで、それだけの問題があるかどうかを本当に自問する必要があります。

2
Vincent Robert

私のプロジェクトでは、 Namespace-Bound Enumeration テクニックを採用して、レガシーおよびサードパーティのコンポーネントからのenumsを処理しました。以下に例を示します。

forward.h:

namespace type
{
    class legacy_type;
    typedef const legacy_type& type;
}

enum.h:

// May be defined here or pulled in via #include.
namespace legacy
{
    enum evil { x , y, z };
}


namespace type
{
    using legacy::evil;

    class legacy_type
    {
    public:
        legacy_type(evil e)
            : e_(e)
        {}

        operator evil() const
        {
            return e_;
        }

    private:
        evil e_;
    };
}

foo.h:

#include "forward.h"

class foo
{
public:
    void f(type::type t);
};

foo.cc:

#include "foo.h"

#include <iostream>
#include "enum.h"

void foo::f(type::type t)
{
    switch (t)
    {
        case legacy::x:
            std::cout << "x" << std::endl;
            break;
        case legacy::y:
            std::cout << "y" << std::endl;
            break;
        case legacy::z:
            std::cout << "z" << std::endl;
            break;
        default:
            std::cout << "default" << std::endl;
    }
}

main.cc:

#include "foo.h"
#include "enum.h"

int main()
{
    foo fu;
    fu.f(legacy::x);

    return 0;
}

foo.hヘッダーはlegacy::evilについて何も知る必要がないことに注意してください。従来のタイプlegacy::evil(ここではmain.cc)を使用するファイルのみにenum.hを含める必要があります。

1
mavam

VCの場合、前方宣言と基本型の指定に関するテストは次のとおりです。

  1. 次のコードは正常にコンパイルされます。
 typedef int myint; 
 enum T; 
 void foo(T * tp)
 {
 * tp =(T)0x12345678; 
} 
 enum T:char 
 {
 A 
}; 

ただし、/ W4の警告が表示されます(/ W3はこの警告を発生しません)

警告C4480:非標準の拡張子が使用されています:列挙型 'T'の基になる型を指定しています

  1. VC(Microsoft(R)32ビットC/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86)は、上記の場合にバグがあります:

    • enum Tを見たとき; VCは、列挙型Tが基本型としてデフォルトの4バイトintを使用することを想定しているため、生成されるアセンブリコードは次のとおりです。
?foo @@ YAXPAW4T @@@ Z PROC; foo 
;ファイルe:\ work\c_cpp\cpp_snippet.cpp 
;行13 
 ebp 
 mov ebp、esp 
を押します。 14行目
 mov eax、DWORD PTR _tp $ [ebp] 
 mov DWORD PTR [eax]、305419896; 12345678H 
;行15 
 pop ebp 
 ret 0 
?foo @@ YAXPAW4T @@@ Z ENDP; foo 

上記のアセンブリコードは、個人的な推測ではなく、/ Fatest.asmから直接抽出されます。 mov DWORD PTR [eax]、305419896が表示されますか? 12345678Hライン?

次のコードスニペットがそれを証明します。

 int main(int argc、char * argv)
 {
 union {
 char ca [4]; 
 T t; 
} a; 
 a.ca [0] = a.ca [1] = a。[ca [2] = a.ca [3] = 1; 
 foo(&a。 t); 
 printf( "%#x、%#x、%#x、%#x\n"、a.ca [0]、a.ca [1]、a.ca [2] 、a.ca [3]); 
 return 0; 
} 

結果:0x78、0x56、0x34、0x12

  • 列挙型Tの前方宣言を削除し、関数fooの定義を列挙型Tの定義の後に移動すると、結果はOKです。

上記のキー命令は次のようになります。

mov BYTE PTR [eax]、120; 00000078H

最終結果は次のとおりです:0x78、0x1、0x1、0x1

値は上書きされないことに注意してください

したがって、VCでenumの前方宣言を使用することは有害と見なされます。

ところで、驚くことではないが、基礎となる型の宣言の構文はC#の構文と同じです。実際には、組み込みシステムと通信するときに基になる型をcharに指定することで3バイトを節約する価値があることがわかりました。これはメモリに制限があります。

1
zhaorufei

これは(ある種の)衝突になったので、反対意見がいくつかあるので、標準からの関連ビットをいくつか紹介します。調査によると、標準では実際に前方宣言を定義しておらず、enumを前方宣言できる、またはできないことを明示的に述べているわけではありません。

まず、dcl.enumのセクション7.2から:

列挙の基になる型は、列挙で定義されたすべての列挙子値を表すことができる整数型です。列挙子の値がintまたはunsigned intに収まらない場合を除き、基になる型がintより大きくなってはならないことを除き、列挙の基になる型として使用される整数型は実装定義です。 enumerator-listが空の場合、基になる型は、列挙に値0の単一の列挙子があるかのようになります。sizeof()の値は、列挙型、列挙型のオブジェクト、または列挙子に適用され、基になる型に適用されるsizeof()。

そのため、列挙型の基礎となる型は実装で定義され、1つの小さな制限があります。

次に、「不完全な型」(3.9)のセクションに目を向けます。これは、前方宣言の標準に近づくのとほぼ同じです。

宣言されているが定義されていないクラス、または不明なサイズまたは不完全な要素タイプの配列は、不完全に定義されたオブジェクトタイプです。

クラスタイプ(「クラスX」など)は、翻訳単位のある時点では不完全であり、後で完了する可能性があります。タイプ「クラスX」は、両方のポイントで同じタイプです。配列オブジェクトの宣言された型は、不完全なクラス型の配列である可能性があるため、不完全です。クラス型が翻訳単位の後半で完成した場合、配列型は完成します。これらの2つのポイントの配列タイプは同じタイプです。配列オブジェクトの宣言された型は、サイズが不明な配列である可能性があるため、翻訳単位のある時点で不完全であり、後で完了します。これらの2点の配列タイプ(「Tの未知の境界の配列」と「NTの配列」)は異なるタイプです。不明なサイズの配列へのポインターの型、またはtypedef宣言で不明なサイズの配列であると定義された型は、完了できません。

そこで、標準では、前方宣言できる型がほぼレイアウトされています。 Enumは存在しなかったため、コンパイラの作成者は一般に、基本型の変数サイズのために、前方宣言を標準で許可されていないと見なします。

それも理にかなっています。列挙型は通常、値による状況で参照され、コンパイラーは実際にそのような状況でのストレージサイズを知る必要があります。ストレージサイズは実装定義であるため、多くのコンパイラはすべての列挙型の基になる型に32ビット値を使用することを選択できます。この時点で、それらを前方宣言することが可能になります。興味深い実験は、Visual Studioで列挙型を前方宣言し、上記で説明したように、sizeof(int)よりも大きい基本型を使用して何が起こるかを強制的に試すことです。

1
Dan Olson

あなたの問題に対する私の解決策は次のいずれかです:

1-列挙型の代わりにintを使用します:CPPファイル(ヘッダーではなく)の匿名ネームスペースでintを宣言します。

namespace
{
   const int FUNCTIONALITY_NORMAL = 0 ;
   const int FUNCTIONALITY_RESTRICTED = 1 ;
   const int FUNCTIONALITY_FOR_PROJECT_X = 2 ;
}

メソッドはプライベートであるため、誰もデータに干渉しません。誰かが無効なデータを送信したかどうかをさらにテストすることもできます。

namespace
{
   const int FUNCTIONALITY_begin = 0 ;
   const int FUNCTIONALITY_NORMAL = 0 ;
   const int FUNCTIONALITY_RESTRICTED = 1 ;
   const int FUNCTIONALITY_FOR_PROJECT_X = 2 ;
   const int FUNCTIONALITY_end = 3 ;

   bool isFunctionalityCorrect(int i)
   {
      return (i >= FUNCTIONALITY_begin) && (i < FUNCTIONALITY_end) ;
   }
}

2:Javaで行われるように、制限されたconstインスタンス化を持つ完全なクラスを作成します。クラスを前方宣言し、CPPファイルでクラスを定義し、列挙型の値のみをインスタンス化します。私はC++でそのようなことをしましたが、列挙型をシミュレートするためのコードが必要なため、結果は望みどおりではありませんでした(コピー構築、演算子=など)。

3:前に提案したように、プライベートに宣言された列挙型を使用します。ユーザーには完全な定義が表示されますが、それを使用したり、プライベートメソッドを使用したりすることはできません。そのため、通常は、クラスを使用してコードを再コンパイルする必要なく、既存のメソッドの列挙とコンテンツを変更できます。

私の推測では、ソリューション3または1のいずれかになります。

0
paercebal