Przekaż deklarację wyliczenia w C ++

263

Próbuję zrobić coś takiego:

enum E;

void Foo(E e);

enum E {A, B, C};

które kompilator odrzuca. Rzuciłem okiem na Google i wydaje się, że konsensus brzmi: „nie możesz tego zrobić”, ale nie rozumiem dlaczego. Czy ktoś może wyjaśnić?

Wyjaśnienie 2: Robię to, ponieważ mam prywatne metody w klasie, która przyjmuje wymieniony wyliczenie i nie chcę ujawniać wartości wyliczenia - więc na przykład nie chcę, aby ktokolwiek wiedział, że E jest zdefiniowane jako

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

ponieważ projekt X nie jest czymś, o czym chcę wiedzieć.

Tak więc chciałem przekazać deklarację wyliczenia, aby móc umieścić metody prywatne w pliku nagłówka, zadeklarować wyliczenie wewnętrznie w procesorze i rozpowszechniać zbudowany plik biblioteki i nagłówek wśród ludzi.

Co do kompilatora - to GCC.

szevvy
źródło
Tyle lat temu i jakoś StackOverflow zwabił mnie z powrotem;) Jako sugestia pośmiertna - po prostu nie rób tego szczególnie w opisywanym scenariuszu. Wolałbym zdefiniować abstrakcyjny interfejs i udostępnić go użytkownikom, a także zachować definicję enum i wszystkie inne szczegóły implementacji z wewnętrzną implementacją, której nikt inny nie widzi po mojej stronie, pozwalając mi robić cokolwiek kiedykolwiek i mieć pełną kontrolę nad tym, kiedy użytkownicy widzą byle co.
RnR

Odpowiedzi:

216

Powodem, dla którego wyliczenie nie może być zadeklarowane, jest to, że bez znajomości wartości kompilator nie może znać wymaganej pamięci dla zmiennej wyliczeniowej. Kompilatory C ++ mogą określać rzeczywistą przestrzeń dyskową na podstawie rozmiaru niezbędnego do przechowywania wszystkich podanych wartości. Jeśli wszystko, co jest widoczne, to deklaracja przesyłania dalej, jednostka tłumacząca nie może wiedzieć, jaki rozmiar pamięci zostanie wybrany - może to być znak, int lub coś innego.


Z sekcji 7.2.5 normy ISO C ++:

Bazowego typu z wyliczenie jest integralną typu, który może reprezentować wszystkie wartości określone w Enumerator wyliczenia. Zdefiniowano implementację, który typ integralny jest używany jako typ bazowy dla wyliczenia, z tym wyjątkiem, że typ bazowy nie może być większy niż, intchyba że wartość modułu wyliczającego nie mieści się w intlub unsigned int. Jeśli lista wyliczająca jest pusta, typem bazowym jest tak, jakby wyliczenie miało jeden wyliczający o wartości 0. Wartość sizeof()zastosowana do typu wyliczeniowego, obiektu typu wyliczeniowego lub wyliczającego jest wartością sizeof()zastosowaną do typ podstawowy.

Ponieważ wywołujący funkcję musi znać rozmiary parametrów, aby poprawnie skonfigurować stos wywołań, liczba wyliczeń na liście wyliczeń musi być znana przed prototypem funkcji.

Aktualizacja: W C ++ 0X zaproponowano i przyjęto składnię deklaracji typów wyliczeniowych do przodu. Propozycję można zobaczyć na stronie http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2764.pdf

KJAWolf
źródło
29
-1. Twoje rozumowanie nie może być poprawne - w przeciwnym razie dlaczego możesz zadeklarować dalej „klasę C;” a następnie zadeklarować prototyp funkcji, który przyjmuje lub zwraca C, zanim w pełni zdefiniuje C?
j_random_hacker
112
@ j_random: Nie możesz użyć klasy, zanim nie zostanie ona w pełni zdefiniowana - możesz użyć tylko wskaźnika lub odwołania do tej klasy, a to dlatego, że ich rozmiary i sposoby działania nie zależą od tego, czym jest klasa.
RnR
27
Rozmiar odwołania lub wskaźnika do obiektu klasy jest ustawiany przez kompilator i niezależnie od rzeczywistego rozmiaru obiektu - jest to rozmiar wskaźników i referencji. Wyliczenie jest obiektem, a jego rozmiar jest potrzebny kompilatorowi do uzyskania dostępu do właściwej pamięci.
KJAWolf
16
Logicznie byłoby w stanie zadeklarować wskaźniki / odniesienia do wyliczeń, gdybyśmy mieli wyliczające do przodu wyliczenia, tak jak robimy to z klasami. Tyle, że często nie masz do czynienia ze wskaźnikami do wyliczeń :)
Pavel Minaev
20
Wiem, że ta dyskusja zakończyła się dawno temu, ale muszę się tutaj zgodzić z @j_random_hacker: tutaj nie chodzi o wskaźnik lub odniesienie do niekompletnych typów, ale o użycie niekompletnych typów w deklaracjach. Ponieważ jest to zgodne z prawem struct S; void foo(S s);(zwróć uwagę, że foojest to tylko zadeklarowane, nieokreślone), nie ma powodu, dla którego nie moglibyśmy zrobić enum E; void foo(E e);tak dobrze. W obu przypadkach rozmiar nie jest potrzebny.
Luc Touraille,
198

Przekazanie deklaracji wyliczeń jest możliwe od wersji C ++ 11. Poprzednio typy wyliczeń nie mogły być deklarowane dalej, ponieważ rozmiar wyliczenia zależy od jego zawartości. Dopóki rozmiar wyliczenia jest określony przez aplikację, można go zadeklarować w przód:

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.
użytkownik119017
źródło
1
Czy istnieje obsługa kompilatora dla tej funkcji? Wydaje się, że GCC 4.5 go nie ma :(
rubenvb
4
@rubenvb Podobnie jak Visual C ++ 11 (2012) blogs.msdn.com/b/vcblog/archive/2011/09/12/10209291.aspx
knatten
Szukałem enum32_t iz twoją odpowiedzią enum XXX: uint32_t {a, b, c};
fantastory
Myślałem, że wyliczenia zakresowe (klasa enum) zostały zaimplementowane w C ++ 11? Jeśli tak, to w jaki sposób są one legalne w C ++ 0X?
Terrabits
1
C ++ 0x była roboczą nazwą dla C ++ 11, @Terrabits, zanim została oficjalnie ustandaryzowana. Logika jest taka, że ​​jeśli wiadomo, że (lub istnieje duże prawdopodobieństwo), że funkcja ta zostanie uwzględniona w zaktualizowanym standardzie, wówczas użycie tej funkcji przed oficjalnym wydaniem standardu zwykle używa nazwy roboczej. (Na przykład kompilatory, które obsługiwały funkcje C ++ 11 przed oficjalną standaryzacją w 2011 r., Miały obsługę C ++ 0x, kompilatory, które obsługiwały funkcje C ++ 17 przed oficjalną standaryzacją, miały obsługę C ++ 1z oraz kompilatory, które obsługują funkcje C ++ 20 teraz (2019) mają obsługę C ++ 2a.)
Justin Time - Przywróć Monikę
79

Dodam tutaj aktualną odpowiedź, biorąc pod uwagę ostatnie zmiany.

Możesz zadeklarować enum w C ++ 11, pod warunkiem, że zadeklarujesz jego typ przechowywania w tym samym czasie. Składnia wygląda następująco:

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

....

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

W rzeczywistości, jeśli funkcja nigdy nie odwołuje się do wartości wyliczenia, w tym momencie nie potrzebujesz kompletnej deklaracji.

Jest to obsługiwane przez G ++ 4.6 i nowsze ( -std=c++0xlub -std=c++11w nowszych wersjach). Visual C ++ 2013 obsługuje to; we wcześniejszych wersjach ma jakieś niestandardowe wsparcie, którego jeszcze nie rozgryzłem - znalazłem sugestię, że prosta deklaracja przekazania jest legalna, ale YMMV.

Tomek
źródło
4
+1, ponieważ jest to jedyna odpowiedź, która wspomina, że ​​musisz zadeklarować typ w deklaracji, a także w swojej definicji.
turoni 28.04.17
Wierzę, że częściowe wsparcie na początku MSVC zostało przeniesione z C ++ / CLI enum classjako rozszerzenie C ++ (przed innymi C ++ 11 enum class), przynajmniej jeśli dobrze pamiętam. Kompilator pozwolił ci określić typ bazowy wyliczenia, ale nie obsługiwał enum classani nie zadeklarował dalej wyliczeń, i ostrzegł cię, że kwalifikowanie modułu wyliczającego zakresem wyliczenia jest niestandardowym rozszerzeniem. Pamiętam, że działał mniej więcej tak samo jak w przypadku określania podstawowego typu w C ++ 11, z wyjątkiem bardziej irytujących, ponieważ musiałeś stłumić ostrzeżenie.
Justin Time - Przywróć Monikę
30

Przekazywanie rzeczy do przodu w C ++ jest bardzo przydatne, ponieważ znacznie przyspiesza czas kompilacji . Można do przodu zadeklarować kilka rzeczy w C ++, w tym: struct, class, function, itd ...

Ale czy możesz przekazać deklarację enumw C ++?

Nie, nie możesz.

Ale dlaczego nie pozwolić na to? Jeśli było to dozwolone, możesz zdefiniować swój enumtyp w pliku nagłówkowym, a enumwartości w pliku źródłowym. Wygląda na to, że powinno być dozwolone, prawda?

Źle.

W C ++ nie ma domyślnego typu, enumpodobnie jak w C # (int). W C ++ Twój enumtyp zostanie określony przez kompilator jako dowolny typ, który będzie pasował do zakresu twoich wartości enum.

Co to znaczy?

Oznacza to, że enumtyp podstawowy nie może być w pełni określony, dopóki nie uzyska się wszystkich wartości enumzdefiniowanych. Których nie możesz rozdzielić deklaracji i definicji swojego enum. I dlatego nie można przekazać deklaracji enumw C ++.

Norma ISO C ++ S7.2.5:

Podstawowy typ wyliczenia jest typem integralnym, który może reprezentować wszystkie wartości wyliczające zdefiniowane w wyliczeniu. Zdefiniowano implementację, który typ integralny jest używany jako typ bazowy dla wyliczenia, z tym wyjątkiem, że typ bazowy nie może być większy niż, intchyba że wartość modułu wyliczającego nie mieści się w intlub unsigned int. Jeśli lista wyliczająca jest pusta, typem bazowym jest tak, jakby wyliczenie miało jeden wyliczający o wartości 0. Wartość sizeof()zastosowana do typu wyliczeniowego, obiektu typu wyliczeniowego lub wyliczającego jest wartością sizeof()zastosowaną do typ podstawowy.

Możesz określić rozmiar wyliczonego typu w C ++ za pomocą sizeofoperatora. Rozmiar wyliczonego typu jest rozmiarem jego podstawowego typu. W ten sposób możesz odgadnąć, jakiego typu używa Twój kompilator enum.

Co się stanie, jeśli enumwyraźnie określisz typ swojego :

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

Czy możesz następnie zgłosić swój enum?

Nie. Ale dlaczego nie?

Określenie typu an enumnie jest faktycznie częścią bieżącego standardu C ++. Jest to rozszerzenie VC ++. Będzie to jednak część C ++ 0x.

Źródło

Brian R. Bondy
źródło
14
Ta odpowiedź jest teraz kilka lat nieaktualna.
Tom
Czas robi z nas wszystkich głupców. Twój komentarz jest teraz kilka lat nieaktualny; odpowiedź na dekadę!
pjcard
14

[Moja odpowiedź jest błędna, ale zostawiłem ją tutaj, ponieważ komentarze są przydatne].

Przekazywanie wyliczeń do przodu jest niestandardowe, ponieważ nie można zagwarantować, że wskaźniki do różnych typów wyliczeń mają ten sam rozmiar. Kompilator może potrzebować zobaczyć definicję, aby wiedzieć, jakie wskaźniki wielkości mogą być używane z tym typem.

W praktyce, przynajmniej we wszystkich popularnych kompilatorach, wskaźniki do wyliczeń mają stały rozmiar. Forward deklaracja wyliczeń jest dostarczana jako rozszerzenie języka na przykład przez Visual C ++.

James Hopkin
źródło
2
-1. Jeśli twoje rozumowanie jest prawidłowe, to samo rozumowanie sugerowałoby, że deklaracje forward typów klas nie mogłyby być użyte do utworzenia wskaźników do tych typów - ale mogą.
j_random_hacker
6
+1. Uzasadnienie jest prawidłowe. Szczególnym przypadkiem są platformy, w których sizeof (char *)> sizeof (int *). Oba mogą być podstawowymi typami wyliczenia, w zależności od zasięgu. Klasy nie mają podstawowych typów, więc analogia jest fałszywa.
MSalters
3
@MSalters: Przykład: „struct S {int x;};” Teraz sizeof (S *) musi być równy rozmiarowi dowolnego innego wskaźnika na strukturę, ponieważ C ++ pozwala zadeklarować i użyć takiego wskaźnika przed zdefiniowaniem S ...
j_random_hacker
1
@MSalters: ... Na platformie, na której sizeof (char *)> sizeof (int *), użycie takiego wskaźnika „pełnego rozmiaru” dla tej konkretnej struktury może być nieefektywne, ale znacznie upraszcza kodowanie - i dokładnie to samo coś może i powinno być zrobione dla typów enum.
j_random_hacker
4
wskaźniki do danych i wskaźniki do funkcji mogą mieć różne rozmiary, ale jestem całkiem pewien, że wskaźniki danych muszą być w obie strony (rzutowane na inny typ wskaźnika danych, a następnie z powrotem na oryginał, muszą nadal działać), co oznacza, że ​​wszystkie wskaźniki danych mają ten sam rozmiar.
Ben Voigt,
7

Rzeczywiście nie ma czegoś takiego jak terminowa deklaracja wyliczenia. Ponieważ definicja wyliczenia nie zawiera żadnego kodu, który mógłby zależeć od innego kodu używającego wyliczenia, zazwyczaj nie ma problemu z całkowitym zdefiniowaniem wyliczenia przy pierwszym deklarowaniu.

Jeśli jedynym użytkiem twojego wyliczenia są prywatne funkcje składowe, możesz zaimplementować enkapsulację, mając wyliczenie jako prywatny członek tej klasy. Wyliczenie wciąż musi być w pełni zdefiniowane w punkcie deklaracji, czyli w ramach definicji klasy. Nie jest to jednak większy problem, ponieważ zadeklarowanie w nim funkcji prywatnego członka i nie jest gorszym ujawnieniem wewnętrznych elementów implementacyjnych niż to.

Jeśli potrzebujesz głębszego ukrywania szczegółów implementacji, możesz rozbić go na abstrakcyjny interfejs, składający się wyłącznie z czysto wirtualnych funkcji i konkretnego, całkowicie ukrytego interfejsu implementującego (dziedziczącego) klasę. Tworzenie instancji klas może być obsługiwane przez fabryczną lub statyczną funkcję elementu interfejsu. W ten sposób nawet prawdziwa nazwa klasy, nie mówiąc już o jej prywatnych funkcjach, nie zostanie ujawniona.

Alexey Feldgendler
źródło
5

Wystarczy zauważyć, że tak naprawdę powodem jest to, że rozmiar wyliczenia nie jest jeszcze znany po deklaracji forward. Cóż, używasz deklaracji forward struktury, aby móc przesuwać wskaźnik dookoła lub odwoływać się do obiektu z miejsca, do którego odwołuje się sama definicja deklarowanej struktury.

Przekazywanie wyliczenia do przodu nie byłoby zbyt przydatne, ponieważ ktoś chciałby móc omijać wartość wyliczeniową. Nie mogłeś nawet mieć do niego wskaźnika, ponieważ niedawno powiedziano mi, że niektóre platformy używają wskaźników różnej wielkości dla char niż dla int lub long. Wszystko zależy więc od zawartości wyliczenia.

Obecny standard C ++ wyraźnie zabrania robienia czegoś podobnego

enum X;

(w 7.1.5.3/1). Ale następnego C ++ standard ze względu na następny rok umożliwia następujące dane, które przekonały mnie problem faktycznie ma do czynienia z typem bazowym:

enum X : int;

Jest znany jako „nieprzezroczysta” deklaracja wyliczeniowa. Możesz nawet użyć X według wartości w następującym kodzie. A jego wyliczacze można później zdefiniować w późniejszej deklaracji wyliczenia. Zobacz 7.2w bieżącym roboczym projekcie.

Johannes Schaub - litb
źródło
4

Zrobiłbym to w ten sposób:

[w nagłówku publicznym]

typedef unsigned long E;

void Foo(E e);

[w nagłówku wewnętrznym]

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

Dodając FORCE_32BIT, zapewniamy, że Econtent kompiluje się do długiej, więc jest wymienna z E.

Laurie Cheers
źródło
1
Oczywiście oznacza to, że (A) typy E i Econtent różnią się i (B) w systemach LP64, sizeof (E) = 2 * sizeof (EContent). Trywialna poprawka: ULONG_MAX, również łatwiejsza do odczytania.
MSalters
2

Jeśli naprawdę nie chcesz, aby twój enum pojawiał się w pliku nagłówkowym ORAZ upewnij się, że jest używany tylko przez prywatne metody, wówczas jednym z rozwiązań może być zastosowanie zasady pimpl.

Jest to technika, która zapewnia ukrywanie wewnętrznych elementów klasy w nagłówkach, po prostu deklarując:

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

Następnie w pliku implementacji (cpp) deklarujesz klasę, która będzie reprezentacją elementów wewnętrznych.

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

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

Należy dynamicznie utworzyć implementację w konstruktorze klas i usunąć ją w destruktorze, a podczas implementowania metody publicznej należy użyć:

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

Korzystanie z pimpl ma swoje zalety, jednym z nich jest to, że oddziela nagłówek klasy od jego implementacji, nie ma potrzeby ponownej kompilacji innych klas przy zmianie implementacji jednej klasy. Innym jest przyspieszenie czasu kompilacji, ponieważ nagłówki są tak proste.

Ale korzystanie z niego jest uciążliwe, dlatego naprawdę powinieneś zadać sobie pytanie, czy po prostu zadeklarowanie wyliczenia jako prywatnego w nagłówku jest tak dużym problemem.

Vincent Robert
źródło
3
struct AImpl; struct A {private: AImpl * pImpl; };
2

Możesz owinąć wyliczenie w strukturze, dodając niektóre konstruktory i konwersje typów, i zamiast tego zadeklaruj strukturę w przód.

#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; \
}

Wygląda na to, że działa: http://ideone.com/TYtP2

Leszek Swirski
źródło
1

Wydaje się, że nie można go zadeklarować w GCC!

Interesująca dyskusja tutaj

prakash
źródło
1

Jest trochę sprzeciwu, odkąd to się podbiło (w pewnym sensie), więc oto kilka istotnych bitów ze standardu. Badania pokazują, że norma tak naprawdę nie definiuje deklaracji forward, ani nie stwierdza wprost, że wyliczenia mogą lub nie mogą być deklarowane forward.

Po pierwsze, z dcl.enum, sekcja 7.2:

Podstawowy typ wyliczenia jest typem integralnym, który może reprezentować wszystkie wartości wyliczające zdefiniowane w wyliczeniu. Zdefiniowano implementację, który typ integralny jest używany jako typ bazowy dla wyliczenia, z tym wyjątkiem, że typ bazowy nie może być większy niż int, chyba że wartość modułu wyliczającego nie może zmieścić się w int lub int podpisany. Jeśli lista wyliczająca jest pusta, typem bazowym jest tak, jakby wyliczenie miało jeden wyliczający o wartości 0. Wartość sizeof () zastosowana do typu wyliczeniowego, obiektu typu wyliczeniowego lub wyliczającego jest wartością sizeof () zastosowany do typu bazowego.

Zatem podstawowy typ wyliczenia jest definiowany implementacyjnie, z jednym niewielkim ograniczeniem.

Następnie przechodzimy do sekcji „typy niekompletne” (3.9), która jest prawie tak bliska, jak zbliżamy się do dowolnego standardu w deklaracjach forward:

Klasa, która została zadeklarowana, ale nie została zdefiniowana, lub tablica o nieznanym rozmiarze lub niepełnym typie elementu, jest niekompletnie zdefiniowanym typem obiektu.

Typ klasy (taki jak „klasa X”) może być w pewnym momencie w jednostce tłumaczeniowej niekompletny, a później uzupełniony; typ „klasa X” jest tego samego typu w obu punktach. Zadeklarowany typ obiektu tablicowego może być tablicą niekompletnego typu klasy, a zatem niekompletnym; jeśli typ klasy zostanie uzupełniony później w jednostce tłumaczeniowej, typ tablicy zostanie uzupełniony; typ tablicy w tych dwóch punktach jest tego samego typu. Zadeklarowany typ obiektu tablicowego może być tablicą o nieznanym rozmiarze, a zatem może być niekompletny w jednym miejscu w jednostce tłumaczenia, a następnie uzupełniony; typy tablic w tych dwóch punktach („tablica nieznanych granic T” i „tablica N T”) są różnymi typami. Typ wskaźnika do tablicy o nieznanym rozmiarze lub typu zdefiniowanego w deklaracji typedef jako tablica o nieznanym rozmiarze,

Tak więc, standard w zasadzie określa typy, które mogą być zadeklarowane w przód. Enum nie było tam, więc autorzy kompilatora ogólnie uważają deklarację forward za niedozwoloną przez standard ze względu na zmienną wielkość jego podstawowego typu.

To też ma sens. Do wyliczeń zwykle odwołuje się w sytuacjach według wartości, a kompilator rzeczywiście musiałby znać wielkość pamięci w takich sytuacjach. Ponieważ rozmiar pamięci jest zdefiniowany w implementacji, wiele kompilatorów może po prostu wybrać 32-bitowe wartości dla podstawowego typu każdego wyliczenia, w którym to momencie możliwe staje się przekazanie dalej deklaracji. Ciekawym eksperymentem może być próba zadeklarowania wyliczenia w studio wizualnym, a następnie zmuszenie go do użycia typu podstawowego większego niż sizeof (int), jak wyjaśniono powyżej, aby zobaczyć, co się stanie.

Dan Olson
źródło
zauważ, że wyraźnie zabrania „enum foo;” w 7.1.5.3/1 (ale jak ze wszystkim, dopóki kompilator ostrzega, może nadal kompilować taki kod, oczywiście)
Johannes Schaub - lit
Dzięki za zwrócenie na to uwagi, to naprawdę ezoteryczny akapit, a jego przetworzenie może zająć tydzień. Ale dobrze wiedzieć, że tam jest.
Dan Olson
nie martw się. niektóre standardowe akapity są naprawdę dziwne :) cóż, rozbudowany specyfikator typów to coś, w którym określasz typ, ale także podajesz coś więcej, aby był jednoznaczny. np. „struct X” zamiast „X” lub „enum Y” zamiast tylko „Y”. potrzebujesz tego, aby stwierdzić, że coś jest naprawdę typem.
Johannes Schaub - litb
więc możesz użyć tego w następujący sposób: „class X * foo;” jeśli X nie został jeszcze zadeklarowany w przód. lub „typename X :: foo” w szablonie do ujednoznacznienia. lub „class link obj;” jeśli istnieje funkcja „link” w tym samym zakresie, która przesłaniałaby klasę o tej samej nazwie.
Johannes Schaub - litb
w 3.4.4 mówi się, że są używane, jeśli jakaś nazwa nietypowa ukrywa nazwę typu. tam najczęściej się je stosuje, z wyjątkiem deklaracji typu „klasa X;” (tutaj jest to jedyne oświadczenie). mówi o nich w nie-szablonach tutaj. jednak 14.6 / 3 wymienia ich użycie w szablonach.
Johannes Schaub - litb
1

W przypadku VC oto test dotyczący deklaracji forward i określenia typu bazowego:

  1. poniższy kod jest skompilowany ok.
    typedef int myint;
    wyliczyć T;
    void foo (T * tp)
    {
        * tp = (T) 0x12345678;
    }
    enum T: char
    {
        ZA
    };

Ale dostałem ostrzeżenie dla / W4 (/ W3 nie wyświetla tego ostrzeżenia)

ostrzeżenie C4480: używane niestandardowe rozszerzenie: określenie typu bazowego dla wyliczenia „T”

  1. VC (Microsoft (R) 32-bitowy kompilator optymalizujący C / C ++ w wersji 15.00.30729.01 dla 80x86) wygląda wadliwie w powyższym przypadku:

    • widząc enum T; VC zakłada, że ​​typ wyliczeniowy T używa domyślnych 4 bajtów int jako typu bazowego, więc wygenerowany kod zestawu to:
    ? foo @@ YAXPAW4T @@@ Z PROC; bla
    ; Plik e: \ work \ c_cpp \ cpp_snippet.cpp
    ; Linia 13
        push ebp
        mov ebp, esp
    ; Linia 14
        mov eax, DWORD PTR _tp $ [ebp]
        mov DWORD PTR [eax], 305419896; 12345678H
    ; Linia 15
        pop ebp
        ret 0
    ? foo @@ YAXPAW4T @@@ Z ENDP; bla

Powyższy kod zestawu jest pobierany bezpośrednio z pliku /Fatest.asm, a nie w moich osobistych przypuszczeniach. Czy widzisz mov DWORD PTR [eax], 305419896; Linia 12345678H?

udowadnia to następujący fragment kodu:

    int main (int argc, char * argv)
    {
        związek {
            char ca [4];
            T t;
        }za;
        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]) ;
        zwraca 0;
    }

wynikiem jest: 0x78, 0x56, 0x34, 0x12

  • po usunięciu deklaracji do przodu enum T i przeniesieniu definicji funkcji foo po definicji enum T: wynik jest OK:

powyższa kluczowa instrukcja staje się:

mov BYTE PTR [eax], 120; 00000078H

końcowy wynik to: 0x78, 0x1, 0x1, 0x1

Uwaga: wartość nie jest nadpisywana

Tak więc zastosowanie deklaracji wyliczeniowej wyliczenia w VC jest uważane za szkodliwe.

BTW, żeby się nie dziwić, składnia deklaracji typu bazowego jest taka sama jak w C #. W praktyce stwierdziłem, że warto oszczędzić 3 bajty, określając typ podstawowy jako char podczas rozmowy z systemem osadzonym, który ma ograniczoną pamięć.

zhaorufei
źródło
1

W swoich projektach zastosowałem technikę wyliczania granic nazw , aby radzić sobie ze enums ze starszych elementów i komponentów innych firm. Oto przykład:

do przodu. 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;
}

Pamiętaj, że foo.hnagłówek nie musi nic wiedzieć legacy::evil. legacy::evilUwzględnić należy tylko pliki, które używają starszego typu (tutaj: main.cc) enum.h.

mavam
źródło
0

Moim rozwiązaniem Twojego problemu byłoby:

1 - użyj int zamiast enums: Zadeklaruj int w anonimowej przestrzeni nazw w pliku CPP (nie w nagłówku):

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

Ponieważ twoje metody są prywatne, nikt nie będzie bałagan z danymi. Możesz nawet przejść dalej, aby sprawdzić, czy ktoś wyśle ​​Ci nieprawidłowe dane:

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: utwórz pełną klasę z ograniczonymi instancjami stałych, jak w Javie. Przekaż zadeklaruj klasę, a następnie zdefiniuj ją w pliku CPP i zaimplementuj tylko wartości podobne do wyliczania. Zrobiłem coś takiego w C ++, a wynik nie był tak satysfakcjonujący, jak to pożądane, ponieważ potrzebował trochę kodu do symulacji wyliczenia (konstrukcja kopii, operator = itp.).

3: Jak wcześniej zaproponowano, użyj enum deklarowanego prywatnie. Pomimo tego, że użytkownik zobaczy pełną definicję, nie będzie w stanie jej używać ani używać metod prywatnych. Zwykle będziesz więc mógł modyfikować wyliczenie i zawartość istniejących metod bez potrzeby ponownej kompilacji kodu przy użyciu swojej klasy.

Domyślam się, że będzie to rozwiązanie 3 lub 1.

paercebal
źródło
-1

Ponieważ wyliczenie może być całkowitym rozmiarem o różnej wielkości (kompilator decyduje, jaki rozmiar ma dany wyliczenie), wskaźnik do wyliczenia może mieć również różny rozmiar, ponieważ jest to typ integralny (znaki mają wskaźniki o innym rozmiarze na niektórych platformach na przykład).

Kompilator nie może więc nawet zadeklarować do przodu wyliczenia i użyć do niego wskaźnika, ponieważ nawet tam potrzebuje rozmiaru wyliczenia.

Carl Seleborg
źródło
-1

Zdefiniuj wyliczenie, aby ograniczyć możliwe wartości elementów tego typu do ograniczonego zestawu. To ograniczenie należy egzekwować w czasie kompilacji.

Podczas przekazywania dalej deklaracji, że później użyjesz „ograniczonego zestawu”, nie dodaje żadnej wartości: kolejny kod musi znać możliwe wartości, aby z niego skorzystać.

Chociaż kompilator jest zaniepokojony rozmiarem typu wyliczanego, zamiar wyliczenia gubi się, gdy przekażesz go dalej.

xtofl
źródło
1
Nie, kolejny kod nie musi znać wartości, aby było to przydatne - w szczególności, jeśli kolejny kod jest jedynie prototypem funkcji przyjmującym lub zwracającym parametry wyliczeniowe, wielkość typu nie jest ważna. Użycie tutaj deklaracji przekazywania może usunąć zależności kompilacji, przyspieszając kompilację.
j_random_hacker
Masz rację. Celem nie jest przestrzeganie wartości, ale typu. Rozwiązany z typami 0x Enum.
xtofl