Nâng cao về kiểu dữ liệu tùy chỉnh: Union và Enum trong C++

C++ cung cấp nhiều công cụ để định nghĩa kiểu dữ liệu tùy chỉnh, trong đó Union và Enum là hai loại quan trọng. Bài viết này sẽ khám phá chi tiết cách sử dụng chúng cũng như các cải tiến từ C++11 trở lên.

1. Enum (Liệt kê)

Enum cho phép định nghĩa một tập hợp các hằng số nguyên có tên, giúp code dễ đọc và an toàn hơn về kiểu.

1.1 Enum truyền thống vs enum class (khuyên dùng enum class)
// Enum truyền thống (không có namespace riêng)
enum Direction { Up, Down, Left, Right };

// Enum class (có phạm vi, C++11)
enum class Result : int {
    OK = 0,
    Error = 1,
    Pending = 2
};

So sánh chi tiết:

Tính năng Enum truyền thống Enum class
Phạm vi Ảnh hưởng ra bên ngoài Giới hạn trong tên enum
Chuyển đổi kiểu Ngầm định sang int Không cho phép ngầm định
Kiểu cơ sở Mặc định int Có thể chỉ định (int, char, uint64_t...)
Khai báo trước Khó khăn Dễ dàng (cần chỉ định kiểu)
Tương thích STL Hạn chế Tốt

Cách viết khuyên dùng:

enum class Priority : uint8_t {
    Low = 0,
    Medium,
    High,
    Critical
};

// Sử dụng
Priority p = Priority::High;
// int x = p;           // Lỗi biên dịch!
// int y = static_cast<int>(p);  // Đúng: chuyển đổi tường minh

// Enum class kết hợp switch rất tiện
switch (p) {
    case Priority::Low:       // xử lý... break;
    case Priority::Medium:    // xử lý... break;
    case Priority::High:
    case Priority::Critical:
        // xử lý khẩn cấp
        break;
}
1.2 Kỹ thuật nâng cao
  • Kiểm tra kiểu cơ sở: std::underlying_type_t<Priority>
  • Tạo alias với using để code ngắn gọn hơn
  • Bit flags với enum class và operator overloading
enum class FileMode : unsigned {
    Read  = 1 << 0,   // 1
    Write = 1 << 1,   // 2
    Exec  = 1 << 2,   // 4
    All   = Read | Write | Exec
};

constexpr FileMode operator|(FileMode a, FileMode b) {
    return static_cast<FileMode>(
        static_cast<unsigned>(a) | static_cast<unsigned>(b));
}

// Sử dụng
FileMode mode = FileMode::Read | FileMode::Write;

2. Union

Union là kiểu dữ liệu cho phép tiết kiệm bộ nhớ bằng cách chia sẻ vùng nhớ giữa các thành viên.

2.1 Cách dùng cơ bản
union Payload {
    int integer;
    float decimal;
    char buffer[8];
};

Payload p;
p.integer = 100;
// sizeof(Payload) == 8 (tùy platform)

Quy tắc quan trọng:

  • Chỉ một thành viên được active tại một thời điểm.
  • Đọc thành viên không active là undefined behavior (UB).
  • C++11 trở về trước: không chứa thành viên có constructor/destructor phức tạp.
  • C++11 trở lên: cho phép nhưng phải quản lý vòng đời thủ công.
2.2 Anonymous Union

Thường dùng trong struct để tiết kiệm bộ nhớ:

struct Message {
    int type;
    union {           // Anonymous union
        int id;
        double price;
        std::string text;  // C++11+: cần quản lý thủ công
    };
};
2.3 Union với thành viên phức tạp
union Holder {
    int number;
    std::string text;     // Có constructor

    Holder() : number(0) {}                    // Khởi tạo int
    Holder(const std::string& s) : text(s) {}
    ~Holder() { if (which == 2) text.~basic_string(); }

    int which = 1;     // Theo dõi thành viên active
};

Khuyên: Tránh viết union thủ công kiểu này, hãy dùng std::variant.

3. std::variant - Union an toàn của C++17

#include <variant>
#include <string>

using Value = std::variant<int, double, std::string>;

Value v = 42;          // int
v = 3.14;              // double
v = "hello";           // string

// Cách 1: dùng get_if
if (auto* p = std::get_if<int>(&v)) {
    std::cout << *p << '\n';
}

// Cách 2: dùng visit (khuyên dùng)
std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) {
        std::cout << "Integer: " << arg << '\n';
    }
    else if constexpr (std::is_same_v<T, double>) {
        std::cout << "Double: " << arg << '\n';
    }
    else if constexpr (std::is_same_v<T, std::string>) {
        std::cout << "String: " << arg << '\n';
    }
}, v);

Ưu điểm:

  • An toàn về kiểu, không UB
  • Tự động quản lý vòng đời
  • Hỗ trợ std::monostate cho trường hợp không có giá trị
  • Chứa được mọi kiểu (kể cả kiểu phức tạp)

Hiệu năng: Gần như tương đương union thủ công nhưng an toàn hơn nhiều.

4. Hướng dẫn thực hành

  1. Enum:
  • Luôn ưu tiên enum class trong code mới.
  • Chỉ dùng enum truyền thống khi tương thích với C.
  • Chỉ định kiểu cơ sở phù hợp (uint8_t, uint32_t...).
  1. Union:
  • Kiểu đơn giản (POD) có thể dùng trực tiếp.
  • Kiểu phức tạp → dùng std::variant.
  • Chỉ dùng union với quản lý thủ công khi cần hiệu năng cực cao.
  1. Kết hợp:
enum class DataType { Integer, Float, Text };
struct Any {
    DataType tag;
    std::variant<int, double, std::string> value;
};
  1. C++20: Dùng [[no_unique_address]] để tối ưu hóa bộ nhớ trong các kiểu phức tạp.

5. Lỗi thường gặp

  • Đọc thành viên không active của union → UB (crash hoặc giá trị rác).
  • Quên static_cast khi dùng enum class (đây là tính năng, không phải bug).
  • Chứa reference trong std::variant (không được, dùng std::reference_wrapper).
  • Cross-platform: chú ý kiểu cơ sở của enum và alignment của union.

Khi nắm vững enum class và std->variant, code C++ sẽ trở nên an toàn hơn, mạnh mẽ hơn và hiện đại hơn. Union và enum truyền thống chỉ còn cần thiết khi làm việc với legacy code, hệ thống nhúng hoặc driver.

Thẻ: C++ enum class Union std::variant C++17

Đăng vào ngày 15 tháng 7 lúc 01:46