C++17: Tiêu chuẩn thực tế
Khám phá các tính năng có tác động mạnh mẽ nhất của C++17: liên kết có cấu trúc, std::Optional/variant/any, std::string_view, std::filesystem, if constexpr và các thuộc tính mới.
Tại sao C++17 lại quan trọng
C++17 thường được gọi là "tiêu chuẩn thực tế" vì nó giới thiệu vô số tính năng giúp đơn giản hóa việc mã hóa hàng ngày mà không yêu cầu kiến thức lập trình siêu mẫu chuyên sâu. Nó loại bỏ các hành trang cũ (nhưstd::auto_ptrvà trigraph), thêm các loại từ vựng (std::optional,std::variant,std::any) và giới thiệu cú pháp giúp mã vừa ngắn hơn vừa rõ ràng hơn. Nếu hôm nay bạn đang viết C++ sản xuất thì C++17 sẽ là đường cơ sở tối thiểu của bạn.
Các ràng buộc có cấu trúc & Câu lệnh khởi tạo
Các liên kết có cấu trúc cho phép bạn giải nén các tập hợp (cấu trúc, mảng, cặp, bộ dữ liệu) thành các biến được đặt tên trong một khai báo duy nhất. Kết hợp với bộ khởi tạoif/switch, chúng loại bỏ nhiều biến tạm thời:
#include <iostream>
#include <map>
#include <string>
#include <tuple>
std::tuple<std::string, int, double> get_employee() {
return {"Alice", 42, 95000.0};
}
int main() {
// Structured binding with tuple
auto [name, age, salary] = get_employee();
std::cout << name << " is " << age << " years old\n";
// Structured binding with map iteration
std::map<std::string, int> scores{{"Alice", 95}, {"Bob", 87}};
for (const auto& [student, score] : scores) {
std::cout << student << ": " << score << "\n";
}
// if with initializer — the variable is scoped to the if/else
if (auto it = scores.find("Alice"); it != scores.end()) {
std::cout << "Found: " << it->second << "\n";
} else {
std::cout << "Not found\n";
}
// 'it' no longer exists here — no scope leakage
// Structured binding with array
int arr[3] = {10, 20, 30};
auto [x, y, z] = arr;
std::cout << x << ", " << y << ", " << z << "\n";
return 0;
}Các loại từ vựng: tùy chọn, biến thể, bất kỳ
std::optionalđại diện cho một giá trị có thể có hoặc không có (thay thế các giá trị trọng điểm và tham số đầu ra).std::variantlà một liên minh an toàn về loại.std::anygiữ bất kỳ loại có thể sao chép nào bằng cách kiểm tra loại thời gian chạy:
#include <iostream>
#include <optional>
#include <variant>
#include <any>
#include <string>
// optional: replaces "return -1 on failure" patterns
std::optional<int> find_index(const std::string& haystack, char needle) {
for (size_t i = 0; i < haystack.size(); ++i) {
if (haystack[i] == needle) return static_cast<int>(i);
}
return std::nullopt; // no value
}
int main() {
// std::optional
auto idx = find_index("hello", 'l');
if (idx.has_value()) {
std::cout << "Found at index " << *idx << "\n"; // 2
}
std::cout << idx.value_or(-1) << "\n"; // safe default
// std::variant — type-safe union
std::variant<int, double, std::string> value = "hello";
std::cout << std::get<std::string>(value) << "\n";
// Visit pattern — exhaustive handling of all types
std::visit([](const auto& v) {
std::cout << "Value: " << v << "\n";
}, value);
value = 3.14; // now holds a double
std::cout << std::get<double>(value) << "\n";
// std::any — truly any type, with runtime checking
std::any data = 42;
std::cout << std::any_cast<int>(data) << "\n";
data = std::string("world");
std::cout << std::any_cast<std::string>(data) << "\n";
return 0;
}string_view & hệ thống tập tin
std::string_viewlà một tham chiếu nhẹ, không sở hữu một chuỗi. Nó tránh các bản sao khi bạn chỉ cần đọc một chuỗi.std::filesystemcung cấp các thao tác tệp và thao tác đường dẫn đa nền tảng, di động:
#include <iostream>
#include <string>
#include <string_view>
#include <filesystem>
namespace fs = std::filesystem;
// string_view avoids copying — just a pointer + length
void print_trimmed(std::string_view sv) {
auto start = sv.find_first_not_of(' ');
auto end = sv.find_last_not_of(' ');
if (start != std::string_view::npos) {
std::cout << sv.substr(start, end - start + 1) << "\n";
}
}
int main() {
std::string name = " Hello, C++17! ";
print_trimmed(name); // no copy — string_view binds to string
print_trimmed(" world "); // no copy — binds to string literal
// std::filesystem — portable path operations
fs::path source_dir = "/home/user/project/src";
std::cout << "Filename: " << source_dir.filename() << "\n";
std::cout << "Parent: " << source_dir.parent_path() << "\n";
std::cout << "Exists: " << fs::exists(source_dir) << "\n";
// Iterate directory entries
for (const auto& entry : fs::directory_iterator(".")) {
if (entry.is_regular_file()) {
std::cout << entry.path().filename()
<< " (" << entry.file_size() << " bytes)\n";
}
}
// Create directories recursively
fs::create_directories("output/logs/2024");
return 0;
}nếu constexpr, CTAD, v.v.
if constexprcho phép phân nhánh tại thời điểm biên dịch bên trong các mẫu — các nhánh không khớp sẽ bị loại bỏ hoàn toàn. Khấu trừ đối số mẫu lớp (CTAD) cho phép bạn bỏ qua các đối số mẫu khi trình biên dịch có thể suy ra chúng. Biểu thức gấp đơn giản hóa các gói tham số mẫu biến đổi:
#include <iostream>
#include <string>
#include <vector>
#include <type_traits>
// if constexpr — branches resolved at compile time
template<typename T>
std::string to_string_safe(T value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::to_string(value);
} else if constexpr (std::is_same_v<T, std::string>) {
return value;
} else {
return "[unsupported type]";
}
}
// Fold expressions — collapse parameter packs
template<typename... Args>
auto sum(Args... args) {
return (args + ...); // unary right fold
}
template<typename... Args>
void print_all(Args&&... args) {
((std::cout << args << " "), ...); // fold over comma
std::cout << "\n";
}
int main() {
std::cout << to_string_safe(42) << "\n";
std::cout << to_string_safe(std::string("hi")) << "\n";
std::cout << sum(1, 2, 3, 4, 5) << "\n"; // 15
print_all(1, "hello", 3.14); // 1 hello 3.14
// CTAD — no need for std::vector<int>
std::vector v{1, 2, 3, 4, 5}; // deduced as vector<int>
std::pair p{"hello", 42}; // deduced as pair<const char*, int>
// Nested namespaces
// namespace A::B::C { } instead of namespace A { namespace B { namespace C { } } }
// Inline variables — can be defined in headers without ODR violations
// inline constexpr int version = 17;
return 0;
}C++17 đã tiêu chuẩn hóa ba thuộc tính quan trọng:
[[nodiscard]] — Cảnh báo nếu giá trị trả về bị loại bỏ. Sử dụng nó trên các hàm có giá trị trả về phải luôn được kiểm tra (mã lỗi, tài nguyên được phân bổ, hàm xuất xưởng).
[[maybe_unused]] — Loại bỏ các cảnh báo về biến/tham số không được sử dụng. Hữu ích cho các biến chỉ được sử dụng trong bản dựng gỡ lỗi hoặc mã dành riêng cho nền tảng.
[[fallthrough]] — Cho biết sự cố có chủ ý trong trường hợpswitch, tắt tiếng các cảnh báo của trình biên dịch.
Tích cực áp dụng[[nodiscard]]trên API của bạn. Mã lỗi bị loại bỏ là một lỗi đang chờ xảy ra.
Không bao giờ trả vềstd::string_viewtham chiếu chuỗi cục bộ. Vìstring_viewkhông sở hữu dữ liệu của mình nên chuỗi cơ bản có thể bị hủy trước khi sử dụng chế độ xem, gây ra hành vi không xác định:
// DANGEROUS — returns a view to a destroyed temporary!
std::string_view bad() {
std::string s = "hello";
return s; // s is destroyed at end of function!
}Rule of thumb: use string_view for parameters (reading data), return std::string cho trả về giá trị (sở hữu dữ liệu).
- Các liên kết có cấu trúc (
auto [a, b] = ...) giải nén các bộ, cặp, cấu trúc và mảng một cách rõ ràng std::optionalthay thế các giá trị trọng điểm;std::variantthay thế các liên kết không an toàn;std::anydành cho các loại thực sự năng độngstd::string_viewtránh các bản sao để truy cập chuỗi chỉ đọc - nhưng hãy cẩn thận với các tham chiếu lơ lửngif constexprcho phép phân nhánh thời gian biên dịch trong các mẫu, loại bỏ bản soạn sẵn SFINAE- CTAD cho phép trình biên dịch suy ra các đối số mẫu:
std::vector v{1,2,3}chỉ hoạt động - Sử dụng
[[nodiscard]]trên các hàm có giá trị trả về không được bỏ qua
Câu đố — Kiểm tra kiến thức
(15 XP)1. `auto [x, y] = std::make_pair(1, 2.0);`làm gì trong C++17?
2. `std::optional<int>`đại diện cho điều gì?
3. Tại sao việc trả về`std::string_view`từ một hàm lại nguy hiểm nếu nó tham chiếu đến chuỗi cục bộ?