it-swarm-vi.tech

Chuyển tiếp khai báo một enum trong C ++

Tôi đang cố gắng làm một cái gì đó như sau:

enum E;

void Foo(E e);

enum E {A, B, C};

mà trình biên dịch từ chối. Tôi đã có một cái nhìn nhanh về Google và sự đồng thuận dường như là "bạn không thể làm được", nhưng tôi không thể hiểu tại sao. Bất cứ ai có thể giải thích?

Làm rõ 2: Tôi đang làm điều này vì tôi có các phương thức riêng tư trong một lớp có enum nói và tôi không muốn các giá trị của enum bị lộ - vì vậy, ví dụ, tôi không muốn ai biết rằng E được định nghĩa là

enum E {
    FUNCTIONALITY_NORMAL, FUNCTIONALITY_RESTRICTED, FUNCTIONALITY_FOR_PROJECT_X
}

vì dự án X không phải là điều tôi muốn người dùng của mình biết.

Vì vậy, tôi muốn chuyển tiếp khai báo enum để tôi có thể đặt các phương thức riêng tư vào tệp tiêu đề, khai báo enum bên trong cpp và phân phối tệp thư viện được xây dựng và tiêu đề cho mọi người.

Đối với trình biên dịch - đó là GCC.

248
szevvy

Lý do enum không thể được khai báo là vì không biết các giá trị, trình biên dịch không thể biết được dung lượng lưu trữ cần thiết cho biến enum. Trình biên dịch C++ được phép chỉ định không gian lưu trữ thực tế dựa trên kích thước cần thiết để chứa tất cả các giá trị được chỉ định. Nếu tất cả những gì có thể nhìn thấy là khai báo chuyển tiếp, đơn vị dịch thuật không thể biết kích thước lưu trữ nào sẽ được chọn - đó có thể là char hoặc int hoặc thứ gì khác.


Từ Mục 7.2.5 của Tiêu chuẩn ISO C++:

Loại cơ bản của kiểu liệt kê là loại tích phân có thể biểu thị tất cả các giá trị liệt kê được xác định trong phép liệt kê. Nó được định nghĩa theo kiểu triển khai được sử dụng làm kiểu tích phân được sử dụng làm kiểu liệt kê cho loại liệt kê ngoại trừ loại cơ sở không được lớn hơn int trừ khi giá trị của một điều tra viên không thể phù hợp với int hoặc unsigned int. Nếu danh sách liệt kê trống trống, loại bên dưới như thể phép liệt kê có một điều tra viên duy nhất có giá trị 0. Giá trị của sizeof() được áp dụng cho liệt kê loại, một đối tượng của kiểu liệt kê hoặc liệt kê, là giá trị của sizeof() được áp dụng cho loại bên dưới.

Vì hàm gọi cho hàm phải biết kích thước của các tham số để thiết lập chính xác ngăn xếp cuộc gọi, nên phải biết trước số liệt kê trong danh sách liệt kê nguyên mẫu chức năng.

Cập nhật: Trong C++ 0X, một cú pháp khai báo trước các kiểu enum đã được đề xuất và chấp nhận. Bạn có thể xem đề xuất tại http://www.open-std.org/jtc1/sc22/wg21/docs/ con/2008/n2764.pdf

201
KJAWolf

Khai báo chuyển tiếp của enums cũng có thể có trong C++ 0x. Trước đây, lý do các loại enum không thể được tuyên bố chuyển tiếp là vì kích thước của bảng liệt kê phụ thuộc vào nội dung của nó. Miễn là kích thước của bảng liệt kê được chỉ định bởi ứng dụng, nó có thể được chuyển tiếp khai báo:

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

Tôi đang thêm một câu trả lời cập nhật ở đây, với những phát triển gần đây.

Bạn có thể chuyển tiếp khai báo một enum trong C++ 11, miễn là bạn khai báo loại lưu trữ của nó cùng một lúc. Cú pháp trông như thế này:

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

....

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

Trong thực tế, nếu hàm không bao giờ đề cập đến các giá trị của phép liệt kê, thì bạn không cần khai báo đầy đủ tại thời điểm đó.

Điều này được hỗ trợ bởi G ++ 4.6 trở đi (-std=c++0x hoặc -std=c++11 trong các phiên bản gần đây hơn). Visual C++ 2013 hỗ trợ này; trong các phiên bản trước, nó có một số loại hỗ trợ không chuẩn mà tôi chưa tìm ra - tôi thấy một số gợi ý rằng một tuyên bố chuyển tiếp đơn giản là hợp pháp, nhưng YMMV.

72
Tom

Chuyển tiếp khai báo mọi thứ trong C++ rất hữu ích vì nó tăng tốc đáng kể thời gian biên dịch . Bạn có thể chuyển tiếp khai báo một số điều trong C++, bao gồm: struct, class, function, v.v ...

Nhưng bạn có thể chuyển tiếp khai báo một enum trong C++ không?

Không, bạn không thể.

Nhưng tại sao không cho phép nó? Nếu nó được cho phép, bạn có thể xác định loại enum của mình trong tệp tiêu đề và các giá trị enum trong tệp nguồn của bạn. Âm thanh như nó nên được cho phép phải không?

Sai rồi.

Trong C++, không có loại mặc định cho enum giống như có trong C # (int). Trong C++, loại enum của bạn sẽ được trình biên dịch xác định là bất kỳ loại nào phù hợp với phạm vi giá trị bạn có cho enum của bạn.

Điều đó nghĩa là gì?

Điều đó có nghĩa là loại cơ bản của enum của bạn không thể được xác định đầy đủ cho đến khi bạn có tất cả các giá trị của enum được xác định. Những người bạn không thể tách biệt khai báo và định nghĩa của enum của bạn. Và do đó, bạn không thể chuyển tiếp khai báo một enum trong C++.

Tiêu chuẩn ISO C++ S7.2.5:

Kiểu liệt kê cơ bản là một kiểu tích phân có thể biểu thị tất cả các giá trị liệt kê được xác định trong phép liệt kê. Nó được định nghĩa theo kiểu triển khai được sử dụng làm kiểu tích phân được sử dụng làm kiểu liệt kê cho loại liệt kê ngoại trừ loại cơ sở không được lớn hơn int trừ khi giá trị của một điều tra viên không thể phù hợp với int hoặc unsigned int. Nếu danh sách điều tra trống, loại bên dưới như thể liệt kê có một điều tra viên có giá trị 0. Giá trị của sizeof() được áp dụng cho loại liệt kê, đối tượng của kiểu liệt kê hoặc liệt kê, là giá trị của sizeof() được áp dụng đến loại cơ bản.

Bạn có thể xác định kích thước của một kiểu liệt kê trong C++ bằng cách sử dụng toán tử sizeof. Kích thước của kiểu liệt kê là kích thước của kiểu cơ bản. Theo cách này, bạn có thể đoán loại trình biên dịch của bạn đang sử dụng cho enum của bạn.

Điều gì xảy ra nếu bạn chỉ định loại enum của bạn rõ ràng như thế này:

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

Sau đó, bạn có thể chuyển tiếp khai báo enum không?

Không. Nhưng tại sao không?

Chỉ định loại enum không thực sự là một phần của tiêu chuẩn C++ hiện tại. Nó là một phần mở rộng VC++. Nó sẽ là một phần của C++ 0x.

Nguồn

30
Brian R. Bondy

[Câu trả lời của tôi là sai, nhưng tôi đã để nó ở đây vì các bình luận rất hữu ích].

Chuyển tiếp khai báo enum là không chuẩn, bởi vì con trỏ đến các loại enum khác nhau không được đảm bảo có cùng kích thước. Trình biên dịch có thể cần phải xem định nghĩa để biết con trỏ kích thước nào có thể được sử dụng với loại này.

Trong thực tế, ít nhất là trên tất cả các trình biên dịch phổ biến, con trỏ tới enum là một kích thước phù hợp. Ví dụ, khai báo chuyển tiếp của enums được cung cấp dưới dạng phần mở rộng ngôn ngữ của Visual C++.

13
James Hopkin

Thực sự không có thứ gọi là enum phía trước. Vì định nghĩa của enum không chứa bất kỳ mã nào có thể phụ thuộc vào mã khác khi sử dụng enum, nên thường không phải là vấn đề để xác định enum hoàn toàn khi bạn khai báo lần đầu tiên.

Nếu việc sử dụng enum duy nhất của bạn là bởi các hàm thành viên riêng, bạn có thể thực hiện đóng gói bằng cách sử dụng enum như một thành viên riêng của lớp đó. Enum vẫn phải được xác định đầy đủ tại điểm khai báo, nghĩa là trong định nghĩa lớp. Tuy nhiên, đây không phải là một vấn đề lớn hơn khi tuyên bố các chức năng thành viên tư nhân ở đó, và không phải là một triển khai tồi tệ hơn của nội bộ thực hiện hơn thế.

Nếu bạn cần một mức độ che giấu sâu hơn cho các chi tiết triển khai của mình, bạn có thể chia nó thành một giao diện trừu tượng, chỉ bao gồm các hàm ảo thuần túy và một lớp cụ thể, được che giấu hoàn toàn, thực hiện lớp (kế thừa) giao diện. Việc tạo các thể hiện của lớp có thể được xử lý bởi một nhà máy hoặc một chức năng thành viên tĩnh của giao diện. Bằng cách đó, ngay cả tên lớp thực, chưa nói đến các chức năng riêng tư của nó, sẽ không bị lộ.

7
Alexey Feldgendler

Chỉ cần lưu ý rằng lý do thực sự rằng kích thước của enum vẫn chưa được biết sau khi khai báo chuyển tiếp. Chà, bạn sử dụng khai báo về phía trước của một cấu trúc để có thể vượt qua một con trỏ xung quanh hoặc tham chiếu đến một đối tượng từ một nơi được giới thiệu trong chính định nghĩa cấu trúc khai báo phía trước.

Chuyển tiếp tuyên bố một enum sẽ không quá hữu ích, bởi vì người ta muốn có thể vượt qua giá trị enum. Bạn thậm chí không thể có một con trỏ tới nó, bởi vì gần đây tôi đã nói rằng một số nền tảng sử dụng các con trỏ có kích thước khác nhau cho char hơn là int hoặc dài. Vì vậy, tất cả phụ thuộc vào nội dung của enum.

Tiêu chuẩn C++ hiện tại không cho phép làm điều gì đó như

enum X;

(trong 7.1.5.3/1). Nhưng Tiêu chuẩn C++ tiếp theo do năm tới cho phép như sau, điều này đã thuyết phục tôi vấn đề thực sự has để làm với loại cơ bản:

enum X : int;

Nó được gọi là một tuyên bố enum "mờ đục". Bạn thậm chí có thể sử dụng X theo giá trị trong đoạn mã sau. Và các điều tra viên của nó sau này có thể được định nghĩa trong một bản kê khai sau này của bảng liệt kê. Xem 7.2 trong dự thảo làm việc hiện tại.

5

Tôi sẽ làm theo cách này:

[trong tiêu đề công khai]

typedef unsigned long E;

void Foo(E e);

[trong tiêu đề nội bộ]

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

Bằng cách thêm FORCE_32BIT, chúng tôi đảm bảo rằng Econtent biên dịch dài, vì vậy nó có thể hoán đổi với E.

4
Laurie Cheers

Có vẻ như nó không thể được tuyên bố trước trong GCC!

Thảo luận thú vị tại đây

2
prakash

Bạn có thể bọc enum trong một cấu trúc, thêm vào một số hàm tạo và chuyển đổi loại và chuyển tiếp khai báo cấu trúc thay thế.

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

Điều này dường như hoạt động: http://ideone.com/TYtP2

2
Leszek Swirski

Nếu bạn thực sự không muốn enum của mình xuất hiện trong tệp tiêu đề VÀ đảm bảo rằng nó chỉ được sử dụng bởi các phương thức riêng tư, thì một giải pháp có thể là đi theo nguyên tắc pimpl.

Đây là một kỹ thuật đảm bảo ẩn nội bộ lớp trong các tiêu đề bằng cách chỉ cần khai báo:

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

Sau đó, trong tệp thực hiện của bạn (cpp), bạn khai báo một lớp sẽ là đại diện của các phần bên trong.

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

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

Bạn phải tự động tạo việc thực hiện trong hàm tạo của lớp và xóa nó trong hàm hủy và khi thực hiện phương thức chung, bạn phải sử dụng:

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

Có những ưu điểm khi sử dụng pimpl, một là nó tách rời tiêu đề lớp của bạn khỏi việc triển khai nó, không cần biên dịch lại các lớp khác khi thay đổi một lớp thực hiện. Một cách khác là tăng tốc thời gian biên dịch của bạn vì các tiêu đề của bạn rất đơn giản.

Nhưng đó là một nỗi đau để sử dụng, vì vậy bạn thực sự nên tự hỏi mình nếu chỉ tuyên bố enum của bạn là riêng tư trong tiêu đề có nhiều rắc rối không.

2
Vincent Robert

Trong các dự án của mình, tôi đã áp dụng kỹ thuật Namespace-Bound Enumutions để xử lý enums từ các thành phần bên thứ ba và bên thứ ba. Đây là một ví dụ:

chuyển tiếp.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;
    }
}

chính.cc:

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

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

    return 0;
}

Lưu ý rằng tiêu đề foo.h không phải biết gì về legacy::evil. Chỉ các tệp sử dụng loại kế thừa legacy::evil (ở đây: main.cc) cần bao gồm enum.h.

1
mavam

Đối với VC, đây là bài kiểm tra về khai báo chuyển tiếp và chỉ định loại cơ bản:

  1. đoạn mã sau được biên dịch ok.
[.__.] typedef int myint; [.__.] enum T; [.__ ____.]} [.__.] enum T: char [.__.] {[.__.] A [.__.]}; [.__.]

Nhưng nhận được cảnh báo cho/W4 (/ W3 không phát sinh cảnh báo này)

cảnh báo C4480: sử dụng tiện ích mở rộng không chuẩn: chỉ định loại cơ bản cho enum 'T'

  1. VC (Microsoft (R) Trình biên dịch tối ưu hóa C/C++ 32 bit Phiên bản 15.00.30729.01 cho 80x86) có vẻ có lỗi trong trường hợp trên:

    • khi thấy enum T; VC giả sử loại enum T sử dụng int 4 byte mặc định làm kiểu cơ bản, vì vậy mã hội được tạo là:
[.__.]? foo @@ YAXPAW4T @@@ Z PROC; foo [.__.]; Tệp e:\work\c_cpp\cpp_snippet.cpp [.__.]; Dòng 13 [.__.] Đẩy ebp [.__.] Mov ebp, đặc biệt [.__.]; Dòng 14 [.__.] Mov eax, DWORD PTR _tp $ [ebp] [.__ 12345678H [.__.]; Dòng 15 [.__.] Pop ebp [.__.] Ret 0 [.__.]? Foo @@ YAXPAW4T @@@ Z ENDP; foo [.__.]

Mã hội ở trên được trích xuất trực tiếp từ /Fatest.asm, không phải là phỏng đoán cá nhân của tôi. Bạn có thấy Mov DWORD PTR [eax], 305419896; Dòng 12345678H?

đoạn mã sau chứng minh điều đó:

[.__.] int main (int argc, char * argv) [.__.] {[.__.] union {[.__.] .]} a; [.___] t); [.___.] , a.ca [3]); [.__.] trả về 0; [.__.]} [.__.]

kết quả là: 0x78, 0x56, 0x34, 0x12

  • sau khi xóa khai báo về phía trước của enum T và di chuyển định nghĩa của hàm foo sau định nghĩa của enum T: kết quả là OK:

hướng dẫn chính ở trên trở thành:

mov BYTE PTR [eax], 120; 00000078H

kết quả cuối cùng là: 0x78, 0x1, 0x1, 0x1

Lưu ý rằng giá trị không bị ghi đè

Vì vậy, việc sử dụng khai báo chuyển tiếp của enum trong VC được coi là có hại.

BTW, để không ngạc nhiên, cú pháp khai báo kiểu cơ bản giống như trong C #. Trong pratice tôi thấy rằng đáng để lưu 3 byte bằng cách chỉ định loại cơ bản là char khi nói chuyện với hệ thống nhúng, bị giới hạn bộ nhớ.

1
zhaorufei

Có một số bất đồng quan điểm vì điều này đã bị lỗi (loại), vì vậy đây là một số bit có liên quan từ tiêu chuẩn. Nghiên cứu cho thấy rằng tiêu chuẩn không thực sự xác định khai báo chuyển tiếp, cũng không nói rõ rằng enums có thể hoặc không thể được tuyên bố chuyển tiếp.

Đầu tiên, từ dcl.enum, phần 7.2:

Kiểu liệt kê cơ bản là một kiểu tích phân có thể biểu thị tất cả các giá trị liệt kê được xác định trong phép liệt kê. Nó được định nghĩa theo kiểu triển khai được sử dụng làm kiểu tích phân được sử dụng làm kiểu liệt kê cho phép liệt kê ngoại trừ kiểu cơ sở không được lớn hơn int trừ khi giá trị của một điều tra viên không thể khớp với int hoặc unsign int. Nếu danh sách liệt kê trống, loại bên dưới như thể liệt kê có một liệt kê duy nhất có giá trị 0. Giá trị của sizeof () được áp dụng cho loại liệt kê, đối tượng của kiểu liệt kê hoặc liệt kê, là giá trị của sizeof () áp dụng cho kiểu cơ bản.

Vì vậy, kiểu cơ bản của một enum được xác định theo thực thi, với một hạn chế nhỏ.

Tiếp theo, chúng ta lật sang phần "các loại không hoàn chỉnh" (3.9), gần giống với bất kỳ tiêu chuẩn nào về khai báo chuyển tiếp:

Một lớp đã được khai báo nhưng không được xác định hoặc một mảng có kích thước không xác định hoặc loại phần tử không hoàn chỉnh, là một loại đối tượng được xác định không đầy đủ.

Một loại lớp (chẳng hạn như "lớp X") có thể không đầy đủ tại một điểm trong một đơn vị dịch thuật và hoàn thành sau này; loại "lớp X" là cùng loại ở cả hai điểm. Kiểu khai báo của một đối tượng mảng có thể là một mảng của kiểu lớp không hoàn chỉnh và do đó không đầy đủ; nếu loại lớp được hoàn thành sau này trong đơn vị dịch, kiểu mảng sẽ hoàn thành; kiểu mảng tại hai điểm đó là cùng loại. Kiểu khai báo của một đối tượng mảng có thể là một mảng có kích thước không xác định và do đó không đầy đủ tại một điểm trong một đơn vị dịch thuật và hoàn thành sau này; các kiểu mảng tại hai điểm đó ("mảng không xác định ràng buộc của T" và "mảng N T") là các loại khác nhau. Không thể hoàn thành loại con trỏ tới mảng có kích thước không xác định hoặc loại được xác định bởi khai báo typedef là một mảng có kích thước không xác định.

Vì vậy, tiêu chuẩn khá nhiều đặt ra các loại có thể được tuyên bố chuyển tiếp. Enum không có ở đó, vì vậy các tác giả biên dịch thường coi việc khai báo về phía trước là không được phép bởi tiêu chuẩn do kích thước thay đổi của loại cơ bản của nó.

Nó có ý nghĩa, quá. Enums thường được tham chiếu trong các tình huống theo giá trị và trình biên dịch thực sự sẽ cần biết kích thước lưu trữ trong các tình huống đó. Do kích thước lưu trữ được xác định thực hiện, nhiều trình biên dịch có thể chỉ chọn sử dụng các giá trị 32 bit cho loại cơ bản của mỗi enum, tại thời điểm đó có thể chuyển tiếp khai báo chúng. Một thử nghiệm thú vị có thể là thử chuyển tiếp khai báo enum trong phòng thu trực quan, sau đó buộc nó sử dụng loại cơ bản lớn hơn sizeof (int) như đã giải thích ở trên để xem điều gì xảy ra.

1
Dan Olson

Giải pháp của tôi cho vấn đề của bạn sẽ là:

1 - sử dụng int thay vì enums: Khai báo ints của bạn trong một không gian tên ẩn danh trong tệp CPP của bạn (không phải trong tiêu đề):

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

Vì phương thức của bạn là riêng tư, không ai sẽ gây rối với dữ liệu. Bạn thậm chí có thể đi xa hơn để kiểm tra nếu ai đó gửi cho bạn dữ liệu không hợp lệ:

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: tạo một lớp đầy đủ với các giới hạn const giới hạn, giống như được thực hiện trong Java. Chuyển tiếp khai báo lớp, và sau đó định nghĩa nó trong tệp CPP và chỉ cung cấp các giá trị giống như enum. Tôi đã làm một cái gì đó tương tự trong C++ và kết quả không được thỏa mãn như mong muốn, vì nó cần một số mã để mô phỏng một enum (bản sao xây dựng, toán tử =, v.v.).

3: Như đề xuất trước đây, sử dụng enum khai báo riêng. Mặc dù thực tế người dùng sẽ thấy định nghĩa đầy đủ của nó, nhưng nó sẽ không thể sử dụng nó, cũng không sử dụng các phương thức riêng tư. Vì vậy, thông thường bạn sẽ có thể sửa đổi enum và nội dung của các phương thức hiện có mà không cần biên dịch lại mã bằng lớp của bạn.

Tôi đoán sẽ là giải pháp 3 hoặc 1.

0
paercebal