Các tính năng mới của C++
Lưu ý: Xét theo thực tế thi đấu thuật toán, bài này sẽ không khảo sát toàn bộ cú pháp, chỉ trình bày những phần có thể dùng trong thi đấu.
Cú pháp trong bài tham chiếu chuẩn C++11. Những chỗ có khác biệt về ngữ nghĩa sẽ lấy C++11 làm chuẩn; các cú pháp C++14, C++17... sẽ được nhắc đến khi phù hợp và có ghi chú rõ.
Từ khóa auto
auto dùng để tự động suy luận kiểu của biến, ví dụ:
1 2 | |
Lưu ý auto sẽ bỏ tham chiếu; nếu không muốn phát sinh chi phí copy, cần chỉ định thủ công:
1 2 3 4 | |
Từ khóa decltype
decltype có thể suy luận kiểu dựa trên thực thể hoặc biểu thức, lưu ý hai cách suy luận khác nhau, dùng sai có thể gây tham chiếu treo. Trong thi đấu ít dùng, ở đây chỉ giới thiệu sơ lược.
1 2 3 4 5 6 7 8 9 10 11 | |
constexpr
Xem thêm biểu thức hằng constexpr (C++11)
Vòng lặp for theo phạm vi
Dùng for theo phạm vi để duyệt đối tượng có thể lặp, hiệu suất tương đương duyệt bằng iterator. Hai cách này thường tốt hơn duyệt theo chỉ số vì không cần định vị theo index.
Cú pháp đơn giản của vòng for theo phạm vi:
1 | |
Ví dụ:
1 2 3 4 | |
Hiệu quả tương đương đoạn sau:
1 2 3 4 | |
Khai báo item-declaration
Khai báo một biến để nhận phần tử trong container bên phải, kiểu biến phải phù hợp với kiểu phần tử. Có thể dùng auto để suy luận, kiểu phức tạp thường dùng auto& để tránh copy.
Khởi tạo phạm vi range-initializer
Phạm vi có thể là bất kỳ đối tượng có thể lặp nào (mảng, hoặc lớp có hàm begin và end). Nếu truyền vào một biểu thức, biểu thức chỉ được tính một lần.
Ví dụ:
1 2 3 4 5 6 7 8 | |
Tự định nghĩa kiểu hỗ trợ for theo phạm vi
Chỉ cần cung cấp hàm thành viên begin và end, kiểu trả về phải hỗ trợ so sánh, tăng và giải tham chiếu (*).
Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
Câu lệnh khởi tạo (C++20)
Trong C++20 có thể dùng câu lệnh khởi tạo để làm một số việc như bộ đếm:
1 2 3 4 5 6 7 8 9 | |
Ràng buộc cấu trúc (C++17)
Ràng buộc cấu trúc (Structured binding) là cú pháp đường tắt của C++17 để trích xuất phần tử hoặc tham chiếu phần tử, ví dụ:
1 2 3 4 5 6 7 8 | |
Lưu ý:
- Số biến bên trái phải bằng số phần tử con của đối tượng bên phải
- Khai báo kiểu cần dùng
auto - Có thể thêm
&để lấy tham chiếu
Khi duyệt map có thể viết:
1 2 3 4 5 6 7 8 | |
std::tuple
Tuple được định nghĩa trong <tuple>, là mở rộng của std::pair, có thể lưu nhiều giá trị kiểu khác nhau. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Từ C++17 có thể dùng structured binding để lấy giá trị:
1 2 3 4 5 6 7 | |
Hàm thành viên
| Hàm | Tác dụng |
|---|---|
operator= |
Gán nội dung của một tuple cho một tuple khác |
swap |
Hoán đổi nội dung của hai tuple |
Ví dụ:
1 2 3 4 | |
Hàm không thành viên
| Hàm | Tác dụng |
|---|---|
make_tuple |
Tạo một tuple, kiểu được xác định theo kiểu các đối số |
std::get |
Truy cập phần tử của tuple |
std::tie |
Gán phần tử tuple cho các biến có sẵn |
operator== ... |
So sánh tuple theo thứ tự từ điển |
std::swap |
Thuật toán std::swap được đặc hóa |
Ví dụ:
1 2 3 4 5 6 7 | |
std::tie gán phần tử tuple vào biến có sẵn, có thể dùng std::ignore để bỏ qua phần tử không cần. Structured binding tạo biến mới (hỗ trợ gán theo giá trị/tham chiếu), và phải nhận tất cả phần tử.
Hàm đối tượng
Đối tượng có thể dùng toán tử gọi hàm operator() được gọi là hàm đối tượng (FunctionObject).
Đây không phải là một tính năng của ngôn ngữ, mà là một khái niệm/yêu cầu, được dùng rộng rãi trong thư viện chuẩn.
Hàm đối tượng có thể chia thành hai loại:
- Con trỏ hàm
- Đối tượng lớp có nạp chồng
operator()
lambda là ví dụ điển hình của loại thứ hai: nó lưu phần bắt vào thành biến thành viên, và nạp chồng toán tử gọi hàm.
Biểu thức Lambda
Xem biểu thức Lambda.
std::function
Lưu ý chi phí hiệu năng
std::function có thể gây chi phí hiệu năng, theo Benchmark thường làm chậm 2 đến 3 lần hoặc hơn.
Vì nó dùng kỹ thuật type erasure, thường được thực hiện qua cơ chế hàm ảo; gọi hàm ảo gây chi phí.
Hãy cân nhắc dùng biểu thức Lambda hoặc hàm đối tượng thay thế.
std::function là một bộ bao hàm tổng quát, định nghĩa trong <functional>.
Một đối tượng std::function có thể lưu, sao chép và gọi bất kỳ đối tượng có thể gọi nào, bao gồm lambda, con trỏ hàm thành viên hoặc các hàm đối tượng khác.
Nếu std::function không chứa đối tượng có thể gọi (ví dụ được khởi tạo mặc định), khi gọi sẽ ném ngoại lệ std::bad_function_call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | |
Hàm mẫu tham số biến đổi
Trước C++11, template lớp và hàm chỉ nhận số lượng tham số template cố định. C++11 cho phép bất kỳ số lượng, bất kỳ kiểu tham số template.
Ở đây chỉ giới thiệu ngắn gọn hàm mẫu tham số biến đổi.
Hàm mẫu fun sau có thể nhận số lượng bất kỳ và kiểu bất kỳ các tham số template:
1 2 | |
paras là một gói tham số hàm (function parameter pack), nhận 0 hoặc nhiều tham số hàm. Clazz là một gói tham số template (template parameter pack), nhận 0 hoặc nhiều đối số template (không kiểu, kiểu hoặc template), khi dùng typename thì chỉ nhận kiểu.
Có thể hiểu đơn giản:
- Gói tham số template thường là các tên kiểu (nhưng cũng có thể là hằng số biên dịch hoặc tên template)
- Gói tham số hàm thường là các tên biến
Khi đó có thể gọi fun như sau:
1 2 3 4 | |
Mở rộng gói tham số
Cú pháp mở rộng gói
Mở rộng gói tham số rất đơn giản, dùng ... để mở rộng và tự động dùng , để phân tách. Ví dụ:
1 2 3 4 5 6 7 8 9 10 | |
Mở rộng gói còn có thể kèm theo toán tử:
1 2 3 4 5 6 7 | |
Hàm kết thúc
Hàm phía trên không thể chạy vì số lượng tham số giảm dần, cuối cùng thành rỗng và báo lỗi.
Cần chỉ định điều kiện dừng, bằng cách viết một hàm thường:
1 2 3 4 5 6 7 8 9 | |
Như vậy, khi số tham số không bằng 0 sẽ gọi template, còn rỗng thì gọi hàm thường, chương trình chạy bình thường.
Biểu thức gập (C++17)
C++17 cung cấp cú pháp đơn giản để xử lý gói tham số hàm, cú pháp như sau (bắt buộc có ngoặc):
( pack op ... )sẽ thành(E1 op (... op (EN-1 op EN)))( ... op pack )sẽ thành(((E1 op E2) op ...) op EN)( pack op ... op init )sẽ thành(E1 op (... op (EN−1 op (EN op I))))( init op ... op pack )sẽ thành((((I op E1) op E2) op ...) op EN)
Ví dụ đơn giản:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Hàm mẫu rút gọn (C++20)
Từ C++20 có thể dùng trực tiếp auto ... làm kiểu tham số để rút gọn hàm mẫu:
1 | |
Lưu ý về bản chất vẫn là hàm mẫu, tương đương:
1 2 3 4 | |
Thư viện ranges (C++20)
Thư viện ranges mở rộng iterator và thuật toán generic, giúp tổ hợp iterator/thuật toán mạnh hơn và giảm lỗi.
Range là dãy có thể duyệt, gồm mảng, container, view...
Khi cần thao tác phức tạp trên container/range, ranges giúp code dễ viết và rõ ràng hơn.
View (góc nhìn)
View là đối tượng nhẹ, dùng cơ chế riêng (như iterator tùy biến) để thực hiện thuật toán, cung cấp thêm cách duyệt cho range.
Trong ranges đã có nhiều view, chia thành hai loại:
- Range factory: tạo ra range đặc biệt, giúp khỏi tự dựng container, giảm chi phí.
- Range adaptor: cung cấp nhiều kiểu duyệt, vừa có thể gọi như hàm, vừa dùng toán tử ống
|để nối, tạo chuỗi.
Range adaptor là range adaptor closure object, cũng thuộc function object, chúng nạp chồng operator| để ghép như ống.
Toán tử ống
| ở đây nên hiểu là toán tử ống, không phải OR bit; cách dùng này lấy từ pipe trong Linux.
Khi thao tác phức tạp vẫn giữ được tính dễ đọc, có các tính chất:
Nếu A, B, C là các range adaptor closure object, R là một range, các chữ khác là tham số hợp lệ, thì
1 | |
tương đương
1 | |
Ví dụ với ranges::take_view và ranges::iota_view:
1 2 3 4 5 6 7 8 9 | |
- Range factory
std::views::iota(0, 6)tạo dãy số từ 0 đến 5 - Range adaptor
std::views::filter(even)lọc range trước, chỉ còn số chẵn - Hai thao tác nối bằng toán tử ống
Đoạn code này không cần cấp phát heap để lưu mỗi bước, việc sinh và lọc diễn ra trong lúc duyệt (cụ thể là tạo iterator, tăng, giải tham chiếu), tức zero overhead.
Đồng thời, vòng đời của range đầu vào tương đương vòng đời các phần tử trong range adaptor. Nếu range bên ngoài (container, range factory) đã bị hủy, thì duyệt view sẽ như giải tham chiếu con trỏ treo, là hành vi không xác định.
Để tránh điều này, cần đảm bảo vòng đời của adaptor nằm trong vòng đời của bất kỳ range nào mà nó dùng.
Range bị hủy thì phần tử trong view đều bị treo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Thuật toán có ràng buộc (Constrained Algorithm)
C++20 cung cấp phiên bản ràng buộc của đa số thuật toán trong không gian tên std::ranges, có thể nhận cặp iterator-sentinel hoặc một range làm tham số, hỗ trợ projection và callable trỏ tới thành viên. Ngoài ra còn thay đổi kiểu trả về để chứa các thông tin hữu ích trong quá trình chạy.
Các thuật toán này có thể hiểu là bản cải tiến của thuật toán thư viện cũ, đều là function object, cung cấp overload và kiểm tra kiểu tham số tốt hơn (dựa trên concept). Trước tiên so sánh std::sort và ranges::sort:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
ranges::sort và sort có cùng cài đặt, nhưng cung cấp overload dựa trên range nên truyền tham số gọn hơn. Các thuật toán khác trong std phần lớn cũng có phiên bản range tương ứng trong ranges.
Dùng các tham số range này cùng với view ở phần trước giúp thao tác phức tạp mà vẫn dễ đọc. Ví dụ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
Kết quả:
6 15 24 15 24 33 24 33 42
Tài liệu tham khảo
Last updated on this page:, Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:OI-wiki
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply