वैरिएडिक टेम्पलेट और फ़ोल्ड एक्सप्रेशन (Variadic Templates & Fold Expressions)
पैरामीटर पैक की मदद से किसी भी संख्या में टेम्पलेट आर्गुमेंट संभालें। रिकर्शन के बिना, सुरुचिपूर्ण ढंग से पैक प्रोसेस करने के लिए फ़ोल्ड एक्सप्रेशन में महारत हासिल करें।
पैरामीटर पैक
वैरिएडिक टेम्पलेट एक पैरामीटर पैक की मदद से मनमानी संख्या में टेम्पलेट आर्गुमेंट स्वीकार करता है। इसके सिंटैक्स में एलिप्सिस (...) इस्तेमाल होता है। पैक दो प्रकार के होते हैं:
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 के फ़ोल्ड एक्सप्रेशन ज़्यादातर मामलों में रिकर्सिव टेम्पलेट की ज़रूरत ख़त्म कर देते हैं। फ़ोल्ड एक्सप्रेशन पैरामीटर पैक के सभी एलिमेंट पर एक बाइनरी ऑपरेटर लागू करता है। इसके चार रूप हैं:
- यूनरी राइट फ़ोल्ड: (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 इनिट-कैप्चर)
- using डिक्लेरेशन (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) ज़्यादातर ऑपरेटर के लिए अमान्य (ill-formed) है। शुरुआती वैल्यू के साथ बाइनरी फ़ोल्ड इस्तेमाल करें: (0 + ... + args)।
4. sizeof... गिनती लौटाता है, बाइट में साइज़ नहीं — sizeof...(args) पैक के एलिमेंट की संख्या देता है, उनका कुल मेमोरी साइज़ नहीं।
जहाँ भी संभव हो, रिकर्सिव वैरिएडिक टेम्पलेट की जगह फ़ोल्ड एक्सप्रेशन को प्राथमिकता दें। वे ज़्यादा संक्षिप्त होते हैं, पढ़ने में आसान होते हैं, और अक्सर तेज़ी से कंपाइल होते हैं। खाली पैक को सुरक्षित ढंग से संभालने के लिए स्पष्ट शुरुआती वैल्यू वाले बाइनरी फ़ोल्ड इस्तेमाल करें। रिकर्सिव एक्सपैंशन को उन मामलों के लिए रखें जहाँ फ़ोल्ड एक्सप्रेशन काफ़ी न हों — उदाहरण के लिए, जब आपको एलिमेंट को जोड़ियों में प्रोसेस करना हो, या जब हर एलिमेंट की प्रोसेसिंग के लिए अलग लॉजिक चाहिए।
- वैरिएडिक टेम्पलेट
typename... Ts(टेम्पलेट पैरामीटर पैक) औरTs... args(फ़ंक्शन पैरामीटर पैक) इस्तेमाल करते हैं sizeof...(args)कंपाइल टाइम पर पैक में एलिमेंट की संख्या लौटाता है- पैक एक्सपैंशन हर एलिमेंट पर एक पैटर्न लागू करता है:
f(transform(args)...)हर एलिमेंट पर transform कॉल करता है - फ़ोल्ड एक्सप्रेशन (C++17) रिकर्सिव एक्सपैंशन की जगह लेते हैं:
(... + args)सभी एलिमेंट का जोड़ निकालता है - खाली पैक को सुरक्षित ढंग से संभालने के लिए शुरुआती वैल्यू वाले बाइनरी फ़ोल्ड इस्तेमाल करें
std::applyटपल को फ़ंक्शन आर्गुमेंट के रूप में अनपैक करता है;std::index_sequenceकंपाइल-टाइम इंडेक्सिंग संभव बनाता है
क्विज़ — अपना ज्ञान परखें
(20 XP)1. `args = {a, b, c}` के लिए `(... + args)` का विस्तार क्या होता है?
2. `f(args...)` और `f(args)...` में क्या अंतर है?
3. `sizeof...(args)` क्या लौटाता है?