enum dùng để làm gì
Struct gom nhiều giá trị liên quan vào một đối tượng. enum (kiểu liệt kê) giải quyết một vấn đề khác: nó đặt tên cho một tập nhỏ, cố định các lựa chọn. Thay vì phải nhớ 0 là "đỏ", 1 là "xanh lá" và 2 là "xanh dương", bạn viết Color::Red và trình biên dịch giữ cho bạn không nhầm.
Dùng enum mỗi khi một biến chỉ có thể là một trong vài trạng thái có tên, như màu đèn giao thông, chất của lá bài hay trạng thái kết nối, giúp code tự giải thích và cho phép trình biên dịch bắt lỗi gõ sai hay thiếu trường hợp, điều mà số nguyên trần không bao giờ làm được.
Khai báo enum
C++ hiện đại có hai loại. Hãy bắt đầu với loại bạn nên dùng gần như mọi lúc: enum class có phạm vi (scoped). Bạn liệt kê các tên, và mỗi enumerator được truy cập qua tên của enum bằng :::
Để ý bạn viết Color::Green, không bao giờ viết Green trần. Bản thân các giá trị chỉ là nhãn: bạn so sánh, gán và truyền chúng đi, nhưng hiếm khi quan tâm con số bên dưới. Mặc định Red là 0, Green là 1 và Blue là 2, đếm tăng dần từ không.
enum thường và enum class
enum kiểu cũ, không có phạm vi (không có từ khóa class), thả các tên của nó thẳng vào phạm vi xung quanh và tự chuyển sang int. Nghe thì tiện, nhưng nó gây ra hai vấn đề thật sự:
enum Color { Red, Green, Blue };
enum Fruit { Apple, Banana, Red }; // error: 'Red' already declared
enum Status { Active, Inactive };
int x = Active; // compiles silently - is this what you meant?
if (Active == Banana) { // compares unrelated enums via int - allowed!
}
Vì các enumerator thường là tên toàn cục, hai enum có thể xung đột chỉ vì dùng chung một nhãn. Và vì chúng tự chuyển thành int, trình biên dịch vui vẻ so sánh giá trị của hai enum chẳng liên quan gì đến nhau. enum class có phạm vi sửa được cả hai: các tên nằm bên trong kiểu, và kiểu đó sẽ không âm thầm biến thành int:
Quy tắc chung: mặc định dùng enum class. Chỉ quay về enum thường khi bạn thật sự cần chuyển đổi ngầm sang int, ví dụ các hằng số cờ kiểu C cũ.
Giá trị tùy chỉnh và kiểu nền
Bạn có thể gán số tường minh cho các enumerator. Những cái bạn bỏ trống sẽ tiếp tục đếm tăng từ cái trước, rất tiện cho những thứ như mã trạng thái HTTP hay cờ bit:
Mọi enum đều dựa trên một kiểu số nguyên, mặc định là int. Bạn có thể cố định nó vào một kiểu nhỏ hơn khi quan tâm đến kích thước, ví dụ lưu nhiều enum trong một cấu trúc được đóng gói chặt, hoặc khớp với một định dạng truyền dữ liệu:
Việc chọn kiểu nền cũng bảo đảm khoảng giá trị mà các giá trị của bạn phải nằm trong: một enum uint8_t không thể chứa giá trị lớn hơn 255.
Chuyển đổi giữa enum và int
enum class có phạm vi không bao giờ chuyển đổi ngầm, và đó chính là mục đích của nó. Khi bạn thật sự cần con số, để in ra, làm chỉ số mảng hay đọc từ tệp, hãy dùng static_cast. Chuyển từ enum sang int luôn an toàn:
Chuyển một int ngược lại thành enum mới là chiều nguy hiểm. Phép ép kiểu không kiểm tra con số đó có ứng với một enumerator thật hay không: nó sẽ trả cho bạn một giá trị về mặt kỹ thuật nằm ngoài tập tên của enum:
Suit s = static_cast<Suit>(2); // fine - that's Clubs
Suit bad = static_cast<Suit>(99); // compiles, but 99 is not a valid Suit
// using `bad` in a switch or as an array index is a lurking bug
Nếu số nguyên đến từ dữ liệu người dùng nhập hoặc từ tệp, hãy tự kiểm tra khoảng giá trị trước khi ép kiểu, nếu không bạn sẽ tạo ra một giá trị mà không case nào xử lý, một nguồn undefined behavior khó thấy về sau.
Dùng enum với switch
Vì enum là "một trong một tập cố định", nó kết hợp hoàn hảo với switch. Khi bạn xử lý đủ mọi enumerator, nhiều trình biên dịch sẽ cảnh báo nếu sau này bạn thêm giá trị mới mà quên xử lý, một lớp an toàn miễn phí mà số nguyên thô không mang lại:
Một điểm dễ nhầm: không có cách nào có sẵn để in ra tên của một enumerator. cout << TrafficLight::Red sẽ không biên dịch được với enum có phạm vi, và ngay cả với enum thường thì nó cũng in ra con số chứ không phải "Red". Một switch nhỏ hoặc bảng tra cứu như ở trên là cách thường dùng để biến enum thành chuỗi dễ đọc.
Tiếp theo: ngoại lệ
Enum và struct giúp bạn mô tả dữ liệu trông như thế nào. Nhưng chương trình thật còn phải xử lý những lúc mọi thứ gặp trục trặc: một tệp không mở được, một con số không phân tích được, một giá trị nằm ngoài khoảng cho phép. C++ xử lý các nhánh lỗi đó bằng ngoại lệ (exception), và đó là nội dung trang tiếp theo.
Câu hỏi thường gặp
enum và enum class trong C++ khác nhau thế nào?
enum thường để các tên của nó tràn ra phạm vi bên ngoài và tự chuyển sang int, gây trùng tên và những phép so sánh vô tình. enum class có phạm vi giữ các tên bên trong enum (Color::Red) và không chịu chuyển sang int nếu thiếu ép kiểu tường minh. Trong C++ hiện đại hãy ưu tiên enum class: nó an toàn kiểu và tránh được các bẫy kinh điển.
Làm sao chuyển enum trong C++ sang int?
enum thường tự chuyển đổi ngầm, nên int n = Red; chạy được luôn. enum class cần ép kiểu tường minh: int n = static_cast<int>(Color::Red);. Để chuyển theo chiều ngược lại, ép int về enum: Color c = static_cast<Color>(2);. Nhưng hãy cẩn thận, lúc chạy chương trình không kiểm tra giá trị đó có phải một enumerator hợp lệ hay không.
Enum đầu tiên trong C++ bắt đầu từ giá trị nào?
Mặc định enumerator đầu tiên là 0, và mỗi enumerator sau lớn hơn cái trước một đơn vị. Vì vậy trong enum class Level { Low, Mid, High };, Low là 0, Mid là 1 và High là 2. Bạn có thể gán giá trị tường minh cho bất kỳ enumerator nào để thay đổi điều này.