Chương 17. std::array, std::optional, std::variant và std::string_view
Mục tiêu
- Thay mảng C thô bằng
std::arraymà không mất gì về RAM/tốc độ. - Biểu diễn "không có giá trị" bằng
std::optionalthay vì số ma thuật (-1,0xFFFF). - Mô hình hoá tập sự kiện đóng bằng
std::variant+std::visit, không vtable, không heap. - Dùng
std::string_viewđể xem chuỗi mà không sao chép.
Câu chuyện: hàm đọc cảm biến trả -1 khi lỗi
Một hàm trả int với quy ước "âm là lỗi" hoạt động cho tới ngày cảm biến trả giá trị âm hợp lệ (nhiệt độ −5 °C). Kiểu trả về không nói lên "có thể không có giá trị"; người gọi quên kiểm tra và code chạy tiếp với dữ liệu rác. C++ hiện đại cho kiểu để nói điều đó ra.
Kiến thức
1. std::array và std::optional
// std::array (kích thước cố định, không heap) và std::optional (thay cho giá trị "đặc biệt" như -1).
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <optional>
#include <string_view>
std::optional<std::uint16_t> parse_adc(std::string_view s) {
if (s.empty() || s.size() > 4) return std::nullopt;
std::uint16_t v = 0;
for (char c : s) {
if (c < '0' || c > '9') return std::nullopt;
v = static_cast<std::uint16_t>(v * 10 + (c - '0'));
}
return v;
}
// Trả về chỉ số phần tử đầu tiên vượt ngưỡng, hoặc nullopt nếu không có.
std::optional<std::size_t> first_above(const std::array<std::uint16_t, 6>& a, std::uint16_t limit) {
for (std::size_t i = 0; i < a.size(); ++i)
if (a[i] > limit) return i;
return std::nullopt;
}
int main() {
const std::array<std::uint16_t, 6> samples{120, 340, 90, 2050, 400, 15};
if (auto idx = first_above(samples, 2000)) {
std::printf("vuot nguong tai [%zu] = %u\n", *idx, static_cast<unsigned>(samples[*idx]));
}
if (!first_above(samples, 5000)) std::puts("khong co mau nao vuot 5000");
for (std::string_view text : {"1234", "12a4", "", "99999"}) {
const int len = static_cast<int>(text.size());
if (auto v = parse_adc(text)) std::printf("'%.*s' -> %u\n", len, text.data(), static_cast<unsigned>(*v));
else std::printf("'%.*s' -> loi\n", len, text.data());
}
std::printf("sizeof(array<u16,6>)=%zu sizeof(optional<u8>)=%zu sizeof(optional<u32>)=%zu\n",
sizeof(samples), sizeof(std::optional<std::uint8_t>), sizeof(std::optional<std::uint32_t>));
}
vuot nguong tai [3] = 2050
khong co mau nao vuot 5000
'1234' -> 1234
'12a4' -> loi
'' -> loi
'99999' -> loi
sizeof(array<u16,6>)=12 sizeof(optional<u8>)=2 sizeof(optional<u32>)=8
std::array<T, N>là mảng C bọc trong struct: kích thước nằm trong kiểu, không phân rã thành con trỏ khi truyền hàm, có.size(),.begin(),.at(); vẫn nằm trên stack/static, không heap. Với-O2,operator[]biên dịch giống hệt mảng thô. Truyềnconst std::array<...>&giữ nguyên thông tin kích thước — khác hẳnuint16_t*(chương 10).std::optional<T>=Tcộng một cờ "có giá trị".first_abovetrảstd::nulloptkhi không tìm thấy, vàif (auto idx = ...)ép người gọi xử lý hai trường hợp. Chi phí: thêm cờ + căn chỉnh (sizeof(optional<uint8_t>)thường 2,optional<uint32_t>thường 8 — chương in ra để bạn tự kiểm chứng trên toolchain của mình).- Không dùng
optionalcho lỗi cần lý do (timeout hay CRC sai?) — với trường hợp đó xemResult<T>ở chương 19.
2. std::string_view — nhìn mà không sao chép
std::string_view là cặp (con trỏ, độ dài), không sở hữu dữ liệu, không heap. parse_adc(std::string_view) nhận được chuỗi literal, mảng char, hay một đoạn của buffer UART mà không cần \0 cuối. Hai bẫy:
- View không sống lâu hơn dữ liệu gốc (dangling view giống dangling pointer).
- Không đảm bảo kết thúc bằng
\0: khi đưa vào API C nhưprintf("%s")phải dùng%.*svới độ dài (như ví dụ), không phảidata()trần.
3. std::variant — sự kiện đóng, kiểm tra đủ nhánh
// std::variant + std::visit: tập sự kiện đóng, kiểm tra đủ nhánh lúc biên dịch, không heap, không vtable.
#include <array>
#include <cstdint>
#include <cstdio>
#include <variant>
struct ButtonPressed { std::uint8_t id; };
struct TimerExpired { std::uint32_t ms; };
struct RxByte { std::uint8_t value; };
using Event = std::variant<ButtonPressed, TimerExpired, RxByte>;
template <class... Ts>
struct Overloaded : Ts... { using Ts::operator()...; };
template <class... Ts>
Overloaded(Ts...) -> Overloaded<Ts...>;
void handle(const Event& e) {
std::visit(Overloaded{
[](const ButtonPressed& b) { std::printf("nut %u nhan\n", static_cast<unsigned>(b.id)); },
[](const TimerExpired& t) { std::printf("timer het %u ms\n", static_cast<unsigned>(t.ms)); },
[](const RxByte& r) { std::printf("nhan byte 0x%02X\n", static_cast<unsigned>(r.value)); },
},
e);
}
int main() {
const std::array<Event, 3> queue{ButtonPressed{2}, TimerExpired{100}, RxByte{0x41}};
for (const auto& e : queue) handle(e);
std::printf("sizeof(Event)=%zu\n", sizeof(Event));
}
nut 2 nhan
timer het 100 ms
nhan byte 0x41
sizeof(Event)=8
std::variant<A, B, C> chứa đúng một trong các kiểu đó, lưu ngay tại chỗ (không heap), kích thước ≈ kiểu lớn nhất + chỉ số kiểu. std::visit với bộ lambda Overloaded gọi nhánh tương ứng; nếu thiếu một kiểu, code không biên dịch — thêm loại sự kiện mới thì trình biên dịch chỉ ra mọi nơi cần cập nhật. So với lớp ảo (chương 14):
std::variant |
Lớp ảo | |
|---|---|---|
| Tập kiểu | Đóng, biết lúc biên dịch | Mở, có thể thêm lớp con |
| Bộ nhớ | Trong đối tượng, không heap | Cần con trỏ/tham chiếu tới đối tượng |
| Thêm thao tác mới | Thêm một visit, không sửa các kiểu |
Thêm hàm ảo, sửa mọi lớp con |
| Thêm kiểu mới | Sửa mọi visit (trình biên dịch báo) |
Thêm một lớp con |
Với hàng đợi sự kiện của máy trạng thái (chương sau) variant thường là lựa chọn gọn nhất.
Lưu ý build -fno-exceptions: std::get<T> ném bad_variant_access khi sai kiểu; thay bằng std::get_if hoặc std::visit (chương 19 dùng get_if).
Góc nhúng
- Đo
sizeof(Event)(in trong ví dụ) và nhân với độ sâu hàng đợi để biết RAM cần —std::array<Event, N>làm hàng đợi tĩnh. std::arrayrất hợp với bảng hằng đặt vào Flash: khai báoconstexpr/static constđể linker đặt vào.rodatathay vì RAM (kiểm tra bằng file.map).- Cả bốn kiểu trong chương là header-only, không cần heap; nhưng luôn kiểm tra kích thước Flash sau khi thêm
<variant>/<optional>với-Ostrên toolchain của bạn.
Lỗi thường gặp
- Dùng
optional::value()rồi build không exception: gọi khi rỗng sẽ némbad_optional_access(hoặc abort). Kiểm trahas_value()trước, hoặc dùng*optsau khi đã kiểm tra. string_viewtrỏ vào buffer đã bị ghi đè hoặc chuỗi tạm đã bị hủy.- Quên
Overloadedphải liệt kê mọi kiểu; thêm nhánh[](auto&&){}để "nuốt hết" sẽ làm mất kiểm tra đủ nhánh. - Dùng
std::arrayrồi vẫn truyền.data()+ tự tay mất kích thước.
Bài tập
- Thêm
struct Fault { std::uint8_t code; };vàoEventvà sửahandle. Quan sát lỗi biên dịch trước khi sửa. - Viết
template <std::size_t N> std::optional<std::size_t> find(const std::array<std::uint8_t, N>&, std::uint8_t). - Viết hàm
split_first(std::string_view s, char sep)trả về phần trước dấu phân cách bằngstring_view(không sao chép). - So sánh bằng lời
std::variant<A,B,C>vàunionC + trườngtypeviết tay:variantchặn được lỗi nào?
Tóm tắt
std::array= mảng C có kích thước trong kiểu, cùng chi phí;std::optionalnói rõ "có thể không có";std::string_viewxem chuỗi không sao chép.std::variant+std::visitlà "union an toàn" cho tập sự kiện đóng, kiểm tra đủ nhánh lúc biên dịch.- Cả bốn đều không heap; chi phí thật là cờ/chỉ số kiểu và căn chỉnh — hãy đo
sizeof.