Variadic Templates & Fold Expressions
Handle any number of template arguments with parameter packs. Master fold expressions for elegant, recursive-free pack processing.
Parameter Packs
Mẫu biến thể chấp nhận số lượng đối số mẫu tùy ý bằng cách sử dụng gói tham số. Cú pháp sử dụng dấu chấm lửng (...). Có hai loại gói:
1. Gói tham số mẫu —template — ghi lại 0 hoặc nhiều đối số loại.
2. Gói tham số hàm —void f(Ts... args)— ghi lại các đối số hàm tương ứng.
Bạn không thể lặp trực tiếp qua một gói trong vòng lặp. Thay vào đó, bạn mở rộng nó bằng toán tử.... Thông tin chi tiết quan trọng là mở rộng gói áp dụng mẫu cho mọi thành phần của gói và tạo ra danh sách được phân tách bằng dấu phẩy.
sizeof...(Ts)vàsizeof...(args)trả về số phần tử trong gói dưới dạngconstexpr std::size_t.
Mở rộng gói đệ quy (Mẫu kế thừa)
Trước biểu thức gấp C++17, cách tiêu chuẩn để xử lý gói là khởi tạo đệ quy: bóc phần tử đầu tiên, xử lý nó và lặp lại phần tử còn lại. Điều này đòi hỏi một trường hợp cơ sở để chấm dứt đệ quy.
#include <iostream>
// Base case: no arguments
void print() {
std::cout << '\n';
}
// Recursive case: process first, recurse on rest
template <typename First, typename... Rest>
void print(const First& first, const Rest&... rest) {
std::cout << first;
if constexpr (sizeof...(rest) > 0) {
std::cout << ", ";
}
print(rest...); // expand rest, recurse
}
int main() {
print(1, 2.5, "hello", 'A');
// Output: 1, 2.5, hello, A
print(); // just a newline
}Biểu thức gấp (C++17)
C++17 biểu thức gấp loại bỏ sự cần thiết của các mẫu đệ quy trong hầu hết các trường hợp. Biểu thức gấp áp dụng toán tử nhị phân trên tất cả các phần tử của gói tham số. Có bốn hình thức:
- Nếp gấp bên phải:(pack op ...)mở rộng thànhe1 op (e2 op (... op eN))
- Nếp gấp bên trái:(... op pack)mở rộng thành((e1 op e2) op ...) op eN
- Nếp gấp phải nhị phân:(pack op ... op init)— bao gồm giá trị ban đầu
- Nếp gấp trái nhị phân:(init op ... op pack)— bao gồm giá trị ban đầu
#include <iostream>
#include <string>
// Sum all arguments — unary left fold with +
template <typename... Ts>
auto sum(Ts... args) {
return (... + args); // ((a1 + a2) + a3) + ...
}
// Print all arguments — binary left fold with <<
template <typename... Ts>
void print(Ts&&... args) {
(std::cout << ... << args) << '\n';
// expands to: ((std::cout << a1) << a2) << a3 ...
}
// Check if ALL arguments satisfy a predicate — unary left fold with &&
template <typename... Ts>
bool all_positive(Ts... args) {
return (... && (args > 0)); // (a1>0) && (a2>0) && ...
}
// Push multiple elements to a vector — fold over comma operator
#include <vector>
template <typename T, typename... Ts>
void push_all(std::vector<T>& vec, Ts&&... args) {
(vec.push_back(std::forward<Ts>(args)), ...);
}
int main() {
std::cout << sum(1, 2, 3, 4, 5) << '\n'; // 15
print("x=", 10, " y=", 20); // x=10 y=20
std::cout << std::boolalpha
<< all_positive(1, 2, 3) << '\n' // true
<< all_positive(1, -2, 3) << '\n'; // false
std::vector<int> v;
push_all(v, 10, 20, 30);
for (int x : v) std::cout << x << ' '; // 10 20 30
std::cout << '\n';
}Bối cảnh mở rộng gói
Mở rộng gói (...) có thể xuất hiện trong nhiều ngữ cảnh, không chỉ các đối số lệnh gọi hàm:
- Đối số hàm:f(args...)hoặcf(transform(args)...)
- Đối số mẫu:std::tuple
- Các lớp cơ sở:class Derived : public Bases... { };
- Danh sách trình khởi tạo:auto list = { args... };
- Chụp Lambda:[args...] { }hoặc[...args = std::move(args)] { }(bắt đầu C++20)
- Sử dụng phần khai báo (C++17):using Bases::operator()...;
mẫu là bất cứ thứ gì xuất hiện trước.... Trongf(transform(args)...), mẫu làtransform(args), mở rộng thànhf(transform(a1), transform(a2), transform(a3)).
Điều quan trọng cần hiểu là:f(args)...không giống vớif(args...). Lệnh đầu tiên mở rộng thànhf(a1), f(a2), f(a3)(ba lệnh gọi riêng biệt trong biểu thức dấu phẩy), trong khi lệnh gọi thứ hai mở rộng thànhf(a1, a2, a3)(một lệnh gọi có ba đối số).
std::tuple & std::apply
std::tuplelà vùng chứa không đồng nhất của thư viện tiêu chuẩn, được xây dựng trên các mẫu đa dạng. std::apply (C++17) gọi một lệnh gọi có các phần tử bộ dữ liệu được giải nén làm đối số. std::index_sequence cho phép lập chỉ mục tại thời điểm biên dịch thành các bộ dữ liệu.
#include <tuple>
#include <iostream>
#include <string>
#include <utility>
// std::apply calls a function with tuple elements as arguments
void greet(const std::string& name, int age) {
std::cout << "Hello " << name << ", age " << age << '\n';
}
// Print all tuple elements using index_sequence
template <typename Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template <typename... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
std::cout << '(';
print_tuple_impl(t, std::index_sequence_for<Ts...>{});
std::cout << ")\n";
}
int main() {
auto person = std::make_tuple(std::string("Alice"), 30);
std::apply(greet, person); // Hello Alice, age 30
auto data = std::make_tuple(1, 3.14, std::string("hello"));
print_tuple(data); // (1, 3.14, hello)
}Những lỗi phổ biến nhất với các mẫu variadic:
1. Quên... — viếtargskhi ý bạn làargs...không mở rộng gói, gây ra lỗi biên dịch.
2. Mở rộng không đúng chỗ —f(g(args)...)vàf(g(args...))có ý nghĩa rất khác nhau. Lần đầu tiên gọigmột lần cho mỗi phần tử; cuộc gọi thứ haigvới tất cả các phần tử.
3. Gói trống —(... + args)với gói trống không được định dạng đúng đối với hầu hết các nhà khai thác. Sử dụng phép gấp nhị phân với giá trị ban đầu:(0 + ... + args).
4. sizeof...trả về số lượng, không phải kích thước tính bằng byte -sizeof...(args)cung cấp số phần tử gói, không phải kích thước bộ nhớ kết hợp của chúng.
Ưu tiên các biểu thức gấp hơn các mẫu biến đổi đệ quy bất cứ khi nào có thể. Chúng ngắn gọn hơn, dễ đọc hơn và thường biên dịch nhanh hơn. Sử dụng nếp gấp nhị phân với giá trị ban đầu rõ ràng để xử lý các gói rỗng một cách an toàn. Dự trữ mở rộng đệ quy cho các trường hợp biểu thức gấp không đủ - ví dụ: khi bạn cần xử lý các phần tử theo cặp hoặc khi xử lý từng phần tử yêu cầu logic khác nhau.
- Các mẫu đa dạng sử dụng
typename... Ts(gói tham số mẫu) vàTs... args(gói tham số chức năng) sizeof...(args)trả về số phần tử trong gói tại thời điểm biên dịch- Việc mở rộng gói áp dụng mẫu cho từng phần tử:
f(transform(args)...)gọi biến đổi trên từng phần tử - Biểu thức gấp (C++17) thay thế mở rộng đệ quy:
(... + args)tính tổng tất cả các phần tử - Sử dụng các nếp gấp nhị phân có giá trị ban đầu để xử lý an toàn các gói rỗng
std::applygiải nén một bộ dưới dạng đối số hàm;std::index_sequencecho phép lập chỉ mục thời gian biên dịch
Câu đố — Kiểm tra kiến thức
(20 XP)1. `(... + args)`mở rộng sang`args = {a, b, c}`để làm gì?
2. Sự khác biệt giữa`f(args...)`và`f(args)...`là gì?
3. `sizeof...(args)`trả về cái gì?