Ép kiểu là gì
Ép kiểu (type casting) nghĩa là chuyển một giá trị từ kiểu này sang kiểu khác: biến một double thành int, một char thành mã số của nó, hay một con trỏ lớp cha thành con trỏ lớp con. C++ tự động thực hiện một số chuyển đổi cho bạn, nhưng chính những chuyển đổi diễn ra âm thầm lại là nơi bug ẩn nấp.
Có hai loại: chuyển đổi ngầm định (implicit) tự xảy ra, và ép kiểu tường minh (explicit) do bạn viết ra. Bạn đã gặp một triệu chứng của chuyện này ở phần toán tử: phép chia số nguyên. Ép kiểu là cách bạn kiểm soát nó.
Chuyển đổi ngầm định
Khi bạn trộn các kiểu số trong một biểu thức, C++ nâng kiểu "nhỏ hơn" lên kiểu "lớn hơn" để hai vế khớp nhau. Thường thì kết quả đúng như bạn muốn.
Rắc rối bắt đầu khi chuyển đổi đi theo chiều ngược lại: từ kiểu rộng hơn sang kiểu hẹp hơn. Đó là chuyển đổi thu hẹp (narrowing), và nó có thể làm mất dữ liệu mà không báo gì.
Từ số thực sang số nguyên sẽ cắt bỏ về phía 0, không làm tròn, nên 3.99 thành 3. Còn nhét 300 vào một char thì bị tràn. Nhiều trình biên dịch cảnh báo ở đây, số khác thì không. Khi bạn thật sự muốn thu hẹp, hãy nói rõ bằng một phép ép kiểu để người đọc sau biết đó là cố ý.
Sửa cái bẫy chia số nguyên
Lý do phổ biến nhất để ép kiểu là phép chia. Khi cả hai toán hạng đều là số nguyên, / thực hiện phép chia số nguyên và bỏ đi phần dư.
Cách sửa là dùng static_cast<double> cho một toán hạng trước phép chia. Một lỗi hay gặp là static_cast<double>(got / total): như vậy là quá muộn, vì got / total đã bằng 0 trước khi phép ép kiểu chạy, nên bạn nhận được 0.0. Hãy ép kiểu toán hạng, không phải kết quả.
static_cast: phép ép kiểu mặc định
C++ cho bạn bốn phép ép kiểu có tên. Phép bạn dùng đến 95% thời gian là static_cast<T>(value), thực hiện các chuyển đổi được định nghĩa rõ giữa các kiểu có liên quan: chuyển đổi số, enum sang int, void* trở lại con trỏ có kiểu, và lên hoặc xuống một hệ phân cấp lớp khi bạn đã biết kiểu.
Hãy ưu tiên static_cast thay cho ép kiểu kiểu C cũ (int)balance. Ép kiểu kiểu C sẽ thử bất kỳ chuyển đổi nào để code biên dịch được, kể cả những phép nguy hiểm bên dưới, nên nó có thể âm thầm gỡ const hoặc diễn giải lại các byte thô. static_cast chỉ cho phép những chuyển đổi mà trình biên dịch thực sự chứng minh được, và cú pháp dài dòng static_cast<...> rất dễ tìm khi review code.
// Avoid - C-style cast, no safety net:
int dollars = (int) balance;
// Prefer - explicit, checked, greppable:
int dollars = static_cast<int>(balance);
Ba phép ép kiểu còn lại (dùng hạn chế)
Các phép ép kiểu còn lại tồn tại cho những việc cụ thể, hẹp. Chỉ dùng chúng khi static_cast thật sự không làm được.
const_cast gỡ bỏ (hoặc thêm) const. Cách dùng hợp lệ duy nhất của nó là gọi một API kiểu C quên đánh dấu tham số là const. Sửa một đối tượng vốn được khai báo const thông qua const_cast là undefined behavior.
void legacyApi(char* msg); // old API, doesn't take const
const char* text = "hello";
legacyApi(const_cast<char*>(text)); // only OK if legacyApi doesn't write to it
reinterpret_cast diễn giải lại mẫu bit thô, ví dụ coi một con trỏ là một địa chỉ số nguyên. Nó không chuyển đổi gì cả và cực kỳ không an toàn; gần như luôn là dấu hiệu bạn nên nghĩ lại thiết kế.
dynamic_cast chuyển một con trỏ hoặc tham chiếu lớp cha xuống kiểu lớp con một cách an toàn lúc chạy, dựa trên kiểu thật của đối tượng. Nó yêu cầu lớp cha đa hình (một lớp có ít nhất một hàm ảo) và trả về nullptr nếu phép ép kiểu không áp dụng được.
Nếu a trỏ đến một Animal khác, dynamic_cast<Dog*> sẽ trả về nullptr và nhánh else sẽ chạy. Đó chính là lý do nó an toàn hơn việc dùng static_cast một cách mù quáng để đi xuống hệ phân cấp.
Những lỗi cần tránh
- Ép kiểu kết quả thay vì toán hạng.
static_cast<double>(a / b)đã làm mất phần thập phân trước đó. Hãy ép kiểuahoặcb. - Nghĩ rằng chuyển số thực sang số nguyên sẽ làm tròn. Nó cắt bỏ:
static_cast<int>(2.99)là2. Muốn làm tròn, dùngstd::round,std::lroundvà các hàm tương tự. - Dùng ép kiểu kiểu C. Nó che giấu phép chuyển đổi nào đang diễn ra. Dùng
static_castthì bạn sẽ nhận lỗi biên dịch khi chuyển đổi không an toàn, thay vì một bất ngờ âm thầm. - Thu hẹp vào kiểu quá nhỏ. Ép
300sangcharhoặc mộtlongrất lớn sangintsẽ quay vòng hoặc tràn. Hãy chọn kiểu đích đủ rộng cho phạm vi giá trị.
Tiếp theo: if-else
Giờ bạn đã chuyển đổi và so sánh giá trị một cách gọn gàng, bước tiếp theo là ra quyết định với chúng. Câu lệnh if-else chạy những đoạn code khác nhau tùy theo một điều kiện có true hay không, nền tảng của mọi chương trình có rẽ nhánh.
Câu hỏi thường gặp
static_cast và ép kiểu kiểu C trong C++ khác nhau thế nào?
Ép kiểu kiểu C như (int)x thử lần lượt mọi phép chuyển đổi, nên có thể âm thầm biến thành một reinterpret_cast nguy hiểm hoặc gỡ bỏ const. static_cast<int>(x) chỉ thực hiện các chuyển đổi có liên quan mà trình biên dịch kiểm chứng được, nên những phép vô lý sẽ bị từ chối. Trong C++ hiện đại, hãy luôn ưu tiên static_cast thay cho ép kiểu kiểu C; nó an toàn hơn và dễ tìm kiếm hơn nhiều.
Làm sao ép kiểu int sang double trong C++?
Dùng static_cast<double>(x). Điều này quan trọng nhất với phép chia: 5 / 2 là phép chia số nguyên và cho ra 2, còn static_cast<double>(5) / 2 cho ra 2.5. Hãy ép kiểu một toán hạng trước khi phép chia diễn ra; ép kiểu kết quả, static_cast<double>(5 / 2), là quá muộn và vẫn cho ra 2.0.
Vì sao ép một giá trị lớn sang kiểu nhỏ hơn lại cho ra số sai trong C++?
Chuyển sang một kiểu không chứa nổi giá trị là phép chuyển đổi thu hẹp (narrowing). Từ số thực sang số nguyên sẽ cắt bỏ phần thập phân (static_cast<int>(3.99) là 3), còn số nguyên vượt phạm vi thì hoặc quay vòng (với kiểu không dấu) hoặc phụ thuộc vào trình biên dịch (với kiểu có dấu). Trình biên dịch thường không ngăn bạn, nên hãy ép kiểu có chủ đích và đảm bảo kiểu đích đủ rộng.