optional, variant, any
Thuộc phần Lập trình hướng đối tượng trong hành trình C++ của Coddy. Bài học 89 trên 104.
C++ hiện đại cung cấp ba kiểu từ vựng trong các header <optional>, <variant> và <any>, giúp bạn xử lý các giá trị có thể không tồn tại, chứa một trong nhiều kiểu dữ liệu hoặc lưu trữ bất kỳ kiểu dữ liệu nào.
std::optional<T> biểu diễn một giá trị có thể tồn tại hoặc không, rất phù hợp với các hàm có thể thất bại mà không ném ngoại lệ:
#include <iostream>
#include <optional>
std::optional<int> findIndex(const std::string& str, char c) {
for (size_t i = 0; i < str.size(); ++i) {
if (str[i] == c) return i;
}
return std::nullopt; // Không có giá trị
}
int main() {
auto result = findIndex("hello", 'l');
if (result.has_value()) {
std::cout << "Found at: " << *result << "\n"; // 2
}
}std::variant<Types...> là một union an toàn về kiểu, luôn chứa chính xác một trong các kiểu đã chỉ định:
#include <iostream>
#include <variant>
#include <string>
int main() {
std::variant<int, double, std::string> data;
data = 42;
std::cout << std::get<int>(data) << "\n";
data = "hello";
if (std::holds_alternative<std::string>(data)) {
std::cout << std::get<std::string>(data) << "\n";
}
}std::any có thể chứa giá trị thuộc bất kỳ kiểu dữ liệu nào, được xác định khi chạy chương trình. Dùng std::any_cast để lấy giá trị:
#include <iostream>
#include <any>
int main() {
std::any value = 10;
std::cout << std::any_cast<int>(value) << "\n";
value = std::string("text");
std::cout << std::any_cast<std::string>(value) << "\n";
}Chọn std::optional cho các giá trị có thể là null, std::variant khi bạn biết các kiểu dữ liệu có thể có tại thời điểm biên dịch, và chỉ dùng std::any khi bạn thực sự cần sự linh hoạt về kiểu dữ liệu tại thời điểm chạy.
Thử thách
DễHãy xây dựng một hệ thống cấu hình để minh họa sức mạnh của các kiểu từ vựng hiện đại trong C++. Bạn sẽ tạo một trình quản lý cài đặt linh hoạt, có thể xử lý các giá trị có thể không tồn tại, có thể chứa nhiều kiểu dữ liệu khác nhau hoặc cần tính linh hoạt về kiểu dữ liệu khi chạy chương trình.
Bạn sẽ sắp xếp code trong ba tệp:
ConfigTypes.h: Định nghĩa các kiểu giá trị cấu hình và các hàm hỗ trợ.Tạo hàm (function) tên là
parseValue, nhận một chuỗi và thử phân tích chuỗi đó thành số nguyên. Nếu chuỗi biểu diễn một số nguyên hợp lệ, hãy trả về số nguyên được bọc trongstd::optional. Nếu phân tích thất bại (chuỗi chứa ký tự không phải số), hãy trả vềstd::nullopt. Để đơn giản, hãy xem một chuỗi là hợp lệ nếu chuỗi chỉ chứa các chữ số (và có thể có dấu trừ ở đầu).Đồng thời, tạo bí danh kiểu tên là
Settingbằngstd::variant, có thể chứa một trong các kiểuint,doublehoặcstd::string.Cuối cùng, tạo hàm tên là
describeSetting, nhận mộtSettingvà trả về chuỗi mô tả kiểu dữ liệu mà giá trị đó chứa cùng giá trị của nó theo định dạng sau:- Với int:
Integer: [value] - Với double:
Double: [value] - Với string:
String: [value]
- Với int:
DynamicStore.h: Tạo một kho khóa giá trị đơn giản bằngstd::any.Định nghĩa lớp
DynamicStorecó thể lưu trữ giá trị thuộc bất kỳ kiểu nào. Lớp này cần có:- Phương thức
setnhận một khóa dạng chuỗi và một giá trịstd::any, rồi lưu chúng bên trong (dùngstd::map) - Phương thức
getnhận một khóa và trả về giá trịstd::any(trả về mộtstd::anyrỗng nếu khóa không tồn tại) - Phương thức
hasKeytrả về true nếu khóa tồn tại
- Phương thức
main.cpp: Đọc ba đầu vào:- Một chuỗi có thể là một số (để kiểm tra
std::optional) - Một ký hiệu kiểu dữ liệu:
int,doublehoặcstring - Một giá trị tương ứng với kiểu dữ liệu đó
Minh họa cả ba kiểu từ vựng:
Đầu tiên, dùng hàm
parseValuevới đầu vào thứ nhất. Nếu chuỗi chứa một giá trị, hãy in raParsed: [value]. Nếu chuỗi rỗng, hãy in raParse failed.Thứ hai, tạo một biến thể
Settingdựa trên ký hiệu kiểu dữ liệu. Nếu kiểu dữ liệu làint, hãy lưu giá trị dưới dạng số nguyên. Nếu làdouble, hãy lưu giá trị dưới dạng số thực. Nếu làstring, hãy lưu giá trị dưới dạng chuỗi. Sau đó, gọidescribeSettingvà in ra kết quả.Thứ ba, tạo một
DynamicStore, lưu chuỗi"config_loaded"dưới khóa"status", sau đó truy xuất chuỗi đó và in ra:Status: [value]. Dùngstd::any_cast<std::string>để lấy giá trị.- Một chuỗi có thể là một số (để kiểm tra
Ví dụ, với các đầu vào 42, int và 100:
Parsed: 42
Integer: 100
Status: config_loadedVới các đầu vào hello, double và 3.14:
Parse failed
Double: 3.14
Status: config_loadedVới các đầu vào -15, string và username:
Parsed: -15
String: username
Status: config_loadedNhớ thêm các thư viện phù hợp: <optional>, <variant>, <any>, <string> và <map> khi cần. Dùng std::holds_alternative và std::get để thao tác với biến thể, và has_value() hoặc chuyển đổi trực tiếp sang boolean để kiểm tra đối tượng optional.
Tự làm thử
#include <iostream>
#include <string>
#include <any>
#include "ConfigTypes.h"
#include "DynamicStore.h"
int main() {
// Đọc ba đầu vào
std::string input1; // Chuỗi có thể là số (để kiểm tra std::optional)
std::string typeIndicator; // Biến chỉ kiểu: int, double hoặc string
std::string value; // Giá trị tương ứng với kiểu đó
std::cin >> input1;
std::cin >> typeIndicator;
std::cin >> value;
// TODO: Phần 1: Kiểm tra std::optional bằng parseValue
// Dùng hàm parseValue với input1
// Nếu có giá trị, in ra "Parsed: [value]"
// Nếu rỗng, in ra "Parse failed"
// TODO: Phần 2: Kiểm tra std::variant với Setting
// Tạo một biến thể Setting dựa trên typeIndicator
// Nếu kiểu là "int", lưu giá trị dưới dạng số nguyên
// Nếu kiểu là "double", lưu giá trị dưới dạng số thực
// Nếu kiểu là "string", lưu giá trị dưới dạng chuỗi
// Sau đó gọi describeSetting và in kết quả ra
// TODO: Phần 3: Kiểm tra std::any với DynamicStore
// Tạo một DynamicStore
// Lưu chuỗi "config_loaded" với khóa "status"
// Lấy giá trị đó và in ra: "Status: [value]"
// Dùng std::any_cast<std::string> để lấy giá trị
return 0;
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng về OOP
Tệp bên ngoàiBiên dịch và tạo bản dựng C++Tệp header và tệp nguồnNamespace và phạm viGiới thiệu về OOP trong C++Lớp và đối tượngCon trỏ thisPhương thức (hàm thành viên)Thuộc tính (dữ liệu thành viên)Cơ bản về hàm khởi tạo và hàm hủyÔn tập: máy tính đơn giản4Thuộc tính của lớp
Thành viên đối tượng và thành viên tĩnhHàm truy xuất và hàm thiết lậpHàm thành viên hằngTừ khóa mutablePhương thức và biến tĩnhHàm và lớp bạnÔn tập: Trình quản lý tài khoản ngân hàng7Kế thừa
Kế thừa cơ bảnMức truy cập trong kế thừaThứ tự gọi hàm khởi tạo và hàm hủyGhi đè phương thứcHàm ảo và VTableĐa kế thừaKế thừa ảoÔn tập: hệ phân cấp nhân viên2Quản lý bộ nhớ
Bộ nhớ ngăn xếp và heapCon trỏ và tham chiếuBộ nhớ động (new/delete)Con trỏ thông minh trong C++RAII trong C++Ôn tập: Trình quản lý mảng động5Tính đóng gói
Phạm vi truy cập trong C++Tìm hiểu sâu về phạm vi truy cậpẨn thông tinStruct và lớpLớp lồng nhau và lớp bên trongÔn tập: hệ thống hồ sơ sinh viên8Đa hình
Đa hình lúc biên dịch và khi chạyNạp chồng hàmÔn lại hàm ảoHàm ảo thuần túyLớp trừu tượngThiết kế interface trong C++Ép kiểu động và RTTIÔn tập: Máy tính hình học3Hàm khởi tạo và hàm hủy
Hàm khởi tạo mặc địnhHàm khởi tạo có tham sốHàm khởi tạo sao chépHàm khởi tạo di chuyểnDanh sách khởi tạoHàm khởi tạo ủy quyềnTìm hiểu sâu về hàm hủyQuy tắc 3 / 5 / 0Ôn tập: lớp chuỗi6Nạp chồng toán tử
Giới thiệu về nạp chồng toán tửNạp chồng toán tử số họcNạp chồng toán tử so sánhToán tử luồngNạp chồng toán tử gánNạp chồng toán tử [] và ()Toán tử chuyển đổi kiểuÔn tập: lớp ma trận9Mẫu
Mẫu hàmMẫu lớpChuyên biệt hóa mẫuMẫu biến tham sốCơ bản về SFINAE và đặc điểm kiểuÔn tập: vùng chứa tổng quát12Các tính năng hiện đại của C++
Ngữ nghĩa di chuyển và giá trị rvalueChuyển tiếp hoàn hảoTìm hiểu sâu về biểu thức lambdastd::function và std::bindconstexpr và constevalRàng buộc cấu trúcoptional, variant, anyTự luyện tập: Trình biên dịch C++ online