قوالب متنوعة وتعبيرات أضعاف
تعامل مع أي عدد من وسيطات القالب باستخدام حزم المعلمات. تعبيرات الطي الرئيسية لمعالجة العبوات الأنيقة والخالية من التكرار.
حزم المعلمات
يقبل القالب المتغير عددًا عشوائيًا من وسيطات القالب باستخدام حزمة المعلمات. يستخدم بناء الجملة علامة الحذف (...). هناك نوعان من الحزم:
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... };
- يلتقط Lambda :[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)(استدعاء واحد بثلاث وسيطات).
الأمراض المنقولة جنسيا::tuple & الأمراض المنقولة جنسيا::تطبيق
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...العدد، وليس الحجم بالبايت — يعطيsizeof...(args)عدد عناصر الحزمة، وليس حجم الذاكرة المجمعة الخاصة بها.
تفضل تعبيرات الطي على القوالب المتنوعة المتكررة كلما أمكن ذلك. فهي أكثر إيجازًا وأسهل في القراءة وغالبًا ما يتم تجميعها بشكل أسرع. استخدم الطيات الثنائية بقيمة أولية واضحة للتعامل مع الحزم الفارغة بأمان. احتفظ بالتوسيع العودي للحالات التي تكون فيها تعبيرات الطية غير كافية - على سبيل المثال، عندما تحتاج إلى معالجة العناصر في أزواج، أو عندما تتطلب معالجة كل عنصر منطقًا مختلفًا.
- تستخدم القوالب المتغيرة
typename... Ts(حزمة معلمات القالب) وTs... args(حزمة معلمات الوظيفة) - تقوم
sizeof...(args)بإرجاع عدد العناصر الموجودة في الحزمة في وقت الترجمة - يطبق توسيع الحزمة نمطًا على كل عنصر: تحويل استدعاءات
f(transform(args)...)على كل عنصر - تحل تعبيرات الطية (C++17) محل التوسيع العودي:
(... + args)يجمع كافة العناصر - استخدم الطيات الثنائية ذات القيمة الأولية للتعامل الآمن مع العبوات الفارغة
- يقوم
std::applyبتفكيك الصف كوسيطات دالة؛ يتيحstd::index_sequenceفهرسة وقت الترجمة
اختبار — اختبر معرفتك
(20 نقطة خبرة)1. ما الذي يتوسع فيه`(... + args)`لـ`args = {a, b, c}`؟
2. ما الفرق بين`f(args...)`و`f(args)...`؟
3. ماذا يعود`sizeof...(args)`؟