Variadic Templates & Fold Expressions
Χειριστείτε οποιονδήποτε αριθμό ορισμάτων προτύπου με πακέτα παραμέτρων. Κύρια αναδιπλούμενες εκφράσεις για κομψή, χωρίς αναδρομική επεξεργασία πακέτων.
Πακέτα παραμέτρων
Ένα variadic template δέχεται έναν αυθαίρετο αριθμό ορισμάτων προτύπου χρησιμοποιώντας ένα πακέτο παραμέτρων. Η σύνταξη χρησιμοποιεί έλλειψη (...). Υπάρχουν δύο είδη συσκευασιών:
1. Πακέτο παραμέτρων προτύπου —template — καταγράφει μηδέν ή περισσότερα ορίσματα τύπου.
2. Πακέτο παραμέτρων συνάρτησης —void f(Ts... args)— καταγράφει τα αντίστοιχα ορίσματα συνάρτησης.
Δεν μπορείτε να κάνετε απευθείας επανάληψη πάνω από ένα πακέτο σε έναν βρόχο. Αντίθετα, επεκτείνετε χρησιμοποιώντας τον τελεστή.... Η βασική ιδέα είναι ότι η επέκταση πακέτου εφαρμόζει ένα μοτίβο σε κάθε στοιχείο του πακέτου και παράγει μια λίστα διαχωρισμένη με κόμματα.
Ταsizeof...(Ts)καιsizeof...(args)επιστρέφουν τον αριθμό των στοιχείων στο πακέτο ωςconstexpr std::size_t.
Επέκταση αναδρομικού πακέτου (μοτίβο παλαιού τύπου)
Πριν από τις αναδιπλούμενες εκφράσεις C++17, ο τυπικός τρόπος επεξεργασίας πακέτων ήταν η αναδρομική παρουσίαση: ξεκολλήστε το πρώτο στοιχείο, επεξεργαστείτε το και επαναλάβετε τα υπόλοιπα. Αυτό απαιτεί μια βασική περίπτωση για τον τερματισμό της αναδρομής.
#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
}Εκφράσεις διπλώματος (C++17)
C++17 εκφράσεις διπλώματος εξαλείφουν την ανάγκη για αναδρομικά πρότυπα στις περισσότερες περιπτώσεις. Μια έκφραση fold εφαρμόζει έναν δυαδικό τελεστή σε όλα τα στοιχεία ενός πακέτου παραμέτρων. Υπάρχουν τέσσερις μορφές:
- Μοναδική δεξιά πτυχή: Το(pack op ...)επεκτείνεται σεe1 op (e2 op (... op eN))
- Μοναδική αριστερή πτυχή: Το(... op pack)επεκτείνεται σε((e1 op e2) op ...) op eN
- Δυαδική δεξιά πτυχή:(pack op ... op init)— περιλαμβάνει μια αρχική τιμή
- Δυαδική αριστερή πτυχή:(init op ... op pack)— περιλαμβάνει μια αρχική τιμή
#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';
}Περιεχόμενα επέκτασης πακέτου
Η επέκταση πακέτου (...) μπορεί να εμφανιστεί σε πολλά περιβάλλοντα, όχι μόνο σε ορίσματα κλήσης συναρτήσεων:
- Ορίσματα συνάρτησης:f(args...)ήf(transform(args)...)
- Ορίσματα προτύπου:std::tuple
- Βασικές τάξεις:class Derived : public Bases... { };
- Λίστες αρχικοποιητών:auto list = { args... };
- Λάμβδα λήψη:[args...] { }ή[...args = std::move(args)] { }(C++20 init-capture)
- Χρήση δηλώσεων (C++17):using Bases::operator()...;
Το μοτίβο είναι ό,τι εμφανίζεται πριν από το.... Στοf(transform(args)...), το μοτίβο είναιtransform(args), το οποίο επεκτείνεται σεf(transform(a1), transform(a2), transform(a3)).
Αυτό είναι κρίσιμο να κατανοήσουμε: τοf(args)...δεν είναι το ίδιο με τοf(args...). Η πρώτη επεκτείνεται σεf(a1), f(a2), f(a3)(τρεις ξεχωριστές κλήσεις σε μια έκφραση κόμματος), ενώ η δεύτερη επεκτείνεται σεf(a1, a2, a3)(μία κλήση με τρία ορίσματα).
std::tuple & std::apply
Τοstd::tupleείναι το ετερογενές κοντέινερ της τυπικής βιβλιοθήκης, που βασίζεται σε ποικίλα πρότυπα. Το std::apply (C++17) καλεί έναν καλούμενο με πολλαπλά στοιχεία αποσυμπιεσμένα ως ορίσματα. Το std::index_sequence επιτρέπει την ευρετηρίαση χρόνου μεταγλώττισης σε πλειάδες.
#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)
}Τα πιο συνηθισμένα λάθη με ποικίλα πρότυπα:
1. Ξεχνώντας το... — γράφονταςargsόταν εννοείτε τοargs...αφήνει το πακέτο χωρίς επέκταση, προκαλώντας σφάλμα μεταγλώττισης.
2. Επέκταση σε λάθος μέρος —f(g(args)...)έναντιf(g(args...))έχουν πολύ διαφορετικές έννοιες. Το πρώτο καλεί τοgμία φορά ανά στοιχείο. το δεύτερο καλεί τοgμε όλα τα στοιχεία.
3. Κενές συσκευασίες — Το(... + args)με άδειο πακέτο δεν έχει σχηματιστεί σωστά για τους περισσότερους χειριστές. Χρησιμοποιήστε ένα δυαδικό δίπλωμα με αρχική τιμή:(0 + ... + args).
4. Το sizeof...επιστρέφει τον αριθμό, όχι το μέγεθος σε byte — Τοsizeof...(args)δίνει τον αριθμό των στοιχείων του πακέτου, όχι το συνδυασμένο μέγεθος μνήμης τους.
Προτιμήστε τις αναδιπλούμενες εκφράσεις έναντι των αναδρομικών μεταβλητών προτύπων όποτε είναι δυνατόν. Είναι πιο συνοπτικά, πιο ευανάγνωστα και συχνά μεταγλωττίζονται πιο γρήγορα. Χρησιμοποιήστε δυαδικές πτυχές με ρητή αρχική τιμή για να χειριστείτε τα άδεια πακέτα με ασφάλεια. Κρατήστε την αναδρομική επέκταση για περιπτώσεις όπου οι εκφράσεις διπλώματος είναι ανεπαρκείς — για παράδειγμα, όταν χρειάζεται να επεξεργαστείτε στοιχεία σε ζεύγη ή όταν η επεξεργασία κάθε στοιχείου απαιτεί διαφορετική λογική.
- Variadic templates use
typename... Ts(template parameter pack) andTs... args(function parameter pack) sizeof...(args)returns the number of elements in a pack at compile time- Pack expansion applies a pattern to each element:
f(transform(args)...)calls transform on each element - Fold expressions (C++17) replace recursive expansion:
(... + args)sums all elements - Use binary folds with an initial value for safe handling of empty packs
std::applyunpacks a tuple as function arguments;std::index_sequenceenables compile-time indexing
Κουίζ — Δοκιμάστε τις γνώσεις σας
(20 XP)1. Σε τι επεκτείνεται το`(... + args)`για το`args = {a, b, c}`;
2. Ποια είναι η διαφορά μεταξύ`f(args...)`και`f(args)...`;
3. Τι επιστρέφει το`sizeof...(args)`;