C++ Requires Keyword: 'Bouncer' cho Template của Gen Z
C++

C++ Requires Keyword: 'Bouncer' cho Template của Gen Z

Author

Admin System

@root

Ngày xuất bản

22 Mar, 2026

Lượt xem

1 Lượt

"requires_keyword"

Chào các 'dev' tương lai! Giảng viên Creyt đây, hôm nay chúng ta sẽ cùng nhau 'mổ xẻ' một 'siêu năng lực' mới toanh trong C++20, thứ mà tôi hay gọi vui là 'anh bouncer' khó tính nhưng cực kỳ có tâm của thế giới template: từ khóa requires.

1. requires keyword là gì và để làm gì?

Thử tưởng tượng thế này: bạn đang 'build' một 'con app' đa năng, có thể xử lý đủ loại dữ liệu từ số, chuỗi, đến cả đối tượng phức tạp. Bạn viết một hàm template siêu 'ngầu' để làm điều đó. Nhưng rồi, 'bug' tới tấp khi bạn truyền vào một kiểu dữ liệu 'lạ hoắc' mà hàm của bạn không 'support'. Trước C++20, compiler sẽ 'quăng' vào mặt bạn cả một 'bãi chiến trường' lỗi biên dịch dài dằng dặc, khó hiểu như 'tiếng Mường cổ'.

Đây chính là lúc requires bước ra sân khấu! requires giống như một 'anh bouncer' ở cửa club vậy. Trước khi một kiểu dữ liệu (template parameter) được phép 'bước vào' và sử dụng trong template của bạn, requires sẽ kiểm tra 'visa' của nó: "Mày có operator+ không? Mày có operator< không? Mày có đủ 'phẩm chất' để tham gia 'party' này không?".

Nói cách khác, requires cho phép bạn định nghĩa các yêu cầu (constraints) một cách rõ ràng và tường minh cho các tham số template ngay tại thời điểm biên dịch. Nếu một kiểu dữ dữ liệu không đáp ứng các yêu cầu đó, compiler sẽ 'báo động đỏ' ngay lập tức với một thông báo lỗi rõ ràngdễ hiểu, thay vì chờ đến khi mọi thứ 'nổ tung' bên trong template.

2. Code Ví Dụ Minh Họa: 'Bouncer' vào việc!

Chúng ta hãy xem xét một ví dụ kinh điển: hàm add hai giá trị. Trước đây, bạn cứ viết thôi, rồi 'cầu trời' là kiểu dữ liệu truyền vào có operator+. Giờ đây, chúng ta 'chơi lớn' hơn:

#include <iostream>
#include <string>
#include <concepts> // Cần include header này cho các concept chuẩn và để định nghĩa concept

// Định nghĩa một concept 'Addable' bằng 'requires'
// Đây là cách 'đặt tên' cho một bộ yêu cầu
template<typename T>
concept Addable = requires(T a, T b) {
    { a + b } -> std::same_as<T>; // Yêu cầu: a + b phải hợp lệ và trả về cùng kiểu T
};

// Định nghĩa một concept 'Printable'
template<typename T>
concept Printable = requires(T val) {
    { std::cout << val }; // Yêu cầu: có thể in ra console bằng operator<<
};

// Hàm template 'sum' chỉ chấp nhận các kiểu dữ liệu là 'Addable' và 'Printable'
template<Addable T, Printable U> // Sử dụng concept làm type parameter
auto sum(T a, U b) {
    std::cout << "Calculating sum of " << a << " and " << b << std::endl;
    return a + b;
}

// Một hàm template khác, sử dụng 'requires' clause trực tiếp
template<typename T>
requires (requires(T x) { {x * 2}; }) // Yêu cầu: T phải có operator* với int
auto double_value(T val) {
    return val * 2;
}

int main() {
    // Case 1: Các kiểu hợp lệ
    int i = 5, j = 10;
    std::cout << "Sum of int: " << sum(i, j) << std::endl; // OK

    double d1 = 3.14, d2 = 2.71;
    std::cout << "Sum of double: " << sum(d1, d2) << std::endl; // OK

    std::string s1 = "Hello ", s2 = "World!";
    std::cout << "Sum of string: " << sum(s1, s2) << std::endl; // OK (string có operator+)

    std::cout << "Doubled int: " << double_value(i) << std::endl; // OK

    // Case 2: Các kiểu không hợp lệ (sẽ gây lỗi biên dịch rõ ràng)
    struct MyStruct {};
    MyStruct m1, m2;
    // sum(m1, m2); // Lỗi biên dịch: MyStruct không Addable và không Printable!
    // Error message: 'MyStruct' does not satisfy 'Addable'
    // Error message: 'MyStruct' does not satisfy 'Printable'

    // int k = 10; // Đã khai báo i ở trên
    // std::cout << double_value(s1) << std::endl; // Lỗi biên dịch: string không có operator* với int
    // Error message: 'std::string' does not satisfy the expression 'requires(std::string x) { {x * 2}; }'

    return 0;
}

Trong ví dụ trên:

  • Chúng ta định nghĩa concept AddablePrintable để gói gọn các yêu cầu. Đây là cách 'đặt tên' cho các bộ kiểm tra của 'anh bouncer'.
  • Hàm sum sử dụng trực tiếp Addable T, Printable U trong template parameter list. Điều này có nghĩa là compiler sẽ kiểm tra ngay lập tức nếu TU thỏa mãn AddablePrintable trước khi biên dịch hàm sum.
  • Hàm double_value sử dụng requires clause trực tiếp sau template parameter list. Đây là cách viết nhanh gọn cho các yêu cầu đơn giản, không cần định nghĩa concept riêng.

Nếu bạn bỏ comment và thử biên dịch sum(m1, m2); hoặc double_value(s1);, bạn sẽ thấy thông báo lỗi cực kỳ 'thân thiện', chỉ rõ MyStruct thiếu operator+ hoặc string không có operator* với int, chứ không phải một 'rừng' lỗi nội bộ của template nữa. 'Xịn' chưa?

Illustration

3. Mẹo (Best Practices) để 'Master' requires

  • Kết hợp với concept: Luôn ưu tiên định nghĩa concept để đặt tên cho các tập hợp yêu cầu. Điều này giúp code dễ đọc, dễ tái sử dụng và dễ bảo trì hơn rất nhiều. Coi concept như bạn đang tạo ra các 'nhãn dán' tiêu chuẩn cho các loại đối tượng.
  • Rõ ràng nhưng không quá khắt khe: Định nghĩa requires clause đủ cụ thể để đảm bảo chức năng, nhưng đừng quá chi tiết đến mức loại bỏ các kiểu hợp lệ khác. Ví dụ, nếu bạn chỉ cần operator+, đừng yêu cầu cả operator- nếu nó không cần thiết.
  • Sử dụng requires expressions cho kiểm tra 'ad-hoc': Đối với các yêu cầu cực kỳ đơn giản, chỉ dùng một lần, bạn có thể dùng requires expressions trực tiếp trong requires clause mà không cần concept riêng.
  • Tận dụng std::same_as, std::convertible_to, v.v.: C++ Standard Library cung cấp nhiều concept có sẵn (<concepts>) rất hữu ích để kiểm tra các mối quan hệ giữa các kiểu.
  • Đọc thông báo lỗi: Với requires, thông báo lỗi đã trở nên 'người' hơn rất nhiều. Đừng bỏ qua chúng! Chúng sẽ chỉ cho bạn chính xác vấn đề nằm ở đâu.

4. Học thuật Harvard: 'Thiết kế theo Hợp đồng' trong Lập trình Generic

Từ góc độ học thuật, requires keyword và Concepts trong C++20 là một sự hiện thực hóa mạnh mẽ của nguyên lý 'Design by Contract' (Thiết kế theo Hợp đồng) trong lập trình generic. Thay vì chỉ dựa vào tài liệu (comment) để mô tả các yêu cầu của template, Concepts cho phép chúng ta 'ghi khắc' các điều kiện tiên quyết (preconditions) và hậu điều kiện (postconditions) trực tiếp vào mã nguồn, và compiler sẽ thực thi các 'hợp đồng' này.

Điều này không chỉ giúp cải thiện đáng kể tính an toàn của kiểu (type safety) mà còn tinh chỉnh quá trình giải quyết quá tải hàm (overload resolution) cho các template, giúp compiler chọn ra phiên bản template phù hợp nhất một cách chính xác hơn. Nó chuyển gánh nặng kiểm tra từ thời gian chạy (runtime) sang thời gian biên dịch (compile-time), giúp phát hiện lỗi sớm hơn, giảm thiểu chi phí gỡ lỗi và tăng cường độ tin cậy của phần mềm.

5. Ví dụ Thực tế: Ai đang 'chơi' với requires?

Mặc dù C++20 Concepts còn khá mới mẻ, nhưng tầm ảnh hưởng của nó đang lan rộng:

  • Thư viện chuẩn C++ (STL): Trong các phiên bản tương lai, STL sẽ được 'concept-ified'. Điều này có nghĩa là các thuật toán như std::sort, std::accumulate sẽ sử dụng concept để đảm bảo rằng các iterator và kiểu dữ liệu bạn truyền vào thực sự hỗ trợ các phép toán cần thiết (ví dụ: std::sort yêu cầu RandomAccessIteratorLessThanComparable). Điều này sẽ biến các lỗi khó hiểu thành thông báo rõ ràng.
  • Thư viện tính toán khoa học/số học: Các thư viện chuyên biệt này thường làm việc với các kiểu số học tùy chỉnh. requires giúp họ đảm bảo rằng các kiểu số học này cung cấp tất cả các phép toán cần thiết (cộng, trừ, nhân, chia, v.v.) trước khi sử dụng chúng trong các thuật toán phức tạp.
  • Framework lập trình game/engine: Khi xây dựng các thành phần game engine tổng quát (ví dụ: hệ thống render, hệ thống vật lý), requires có thể được dùng để kiểm tra xem một loại component có cung cấp các interface/hàm cần thiết để tích hợp vào hệ thống hay không.
  • Thư viện xử lý dữ liệu: Các thư viện thao tác với các cấu trúc dữ liệu phức tạp có thể dùng requires để đảm bảo rằng các kiểu dữ liệu lưu trữ có thể được serialize, deserialize, hoặc so sánh theo một cách cụ thể.

6. Thử nghiệm và Hướng dẫn nên dùng cho case nào?

Thử nghiệm:

Cách tốt nhất để thử nghiệm là 'bắt tay vào làm'. Hãy tạo một project C++20 (đảm bảo compiler của bạn hỗ trợ C++20, ví dụ: GCC 10+, Clang 10+), sau đó:

  1. Viết một hàm template đơn giản (ví dụ: tìm max của hai giá trị).
  2. Thêm một requires clause để đảm bảo kiểu dữ liệu có thể so sánh được (operator<).
  3. Thử gọi hàm với int, double, std::string (OK).
  4. Thử gọi hàm với một struct tùy chỉnh không có operator< (sẽ lỗi, và bạn sẽ thấy thông báo rõ ràng).
  5. Sau đó, định nghĩa một concept Comparable và refactor lại hàm của bạn để sử dụng concept đó.

Nên dùng cho case nào?

  • Bất cứ khi nào bạn viết template: Đây là 'quy tắc vàng'. Nếu bạn đang viết một hàm hoặc class template, hãy nghĩ đến việc sử dụng requires để định nghĩa rõ ràng các yêu cầu của template parameter.
  • Khi bạn muốn cải thiện thông báo lỗi của template: Nếu bạn đã từng 'đau đầu' với lỗi template, requires là 'cứu tinh' của bạn.
  • Khi bạn muốn làm rõ ý định của code: requires giúp người đọc code hiểu ngay lập tức các điều kiện cần thiết cho template hoạt động.
  • Khi bạn muốn tạo ra các thư viện generic mạnh mẽ và an toàn: Đặc biệt hữu ích cho các nhà phát triển thư viện.

Nói tóm lại, requires keyword và Concepts không chỉ là một tính năng mới 'cool ngầu' của C++20 mà còn là một công cụ mạnh mẽ giúp chúng ta viết code generic an toàn hơn, dễ hiểu hơn và dễ bảo trì hơn. Hãy 'take note' ngay và bắt đầu áp dụng nó vào các project của mình nhé các 'dev'!

Thuộc Series: C++

Bài giảng này được tự động xuất bản ngẫu nhiên từ thư viện kiến thức. Đừng quên đón xem các Từ khoá Hướng Dẫn tiếp theo nhé!

#tech #cyberpunk #laravel
Chỉnh sửa bài viết

Bình luận (0)

Vui lòng Đăng Nhập để Bình luận

Hỗ trợ Markdown cơ bản
Nguyễn Văn A
1 ngày trước

Tính năng này đỉnh quá ad ơi, chờ mãi mới thấy một blog Tiếng Việt có UI/UX xịn như vầy!