رفتن به محتوا

قالب‌های متغیر-آرگومان و عبارات تا (Variadic Templates & Fold Expressions)

هر تعداد آرگومان قالب را با بسته‌های پارامتر مدیریت کنید. عبارات تا را برای پردازش ظریف و بدون بازگشت بسته‌ها تسلط یابید.

بسته‌های پارامتر (Parameter Packs)

یک قالب متغیر-آرگومان (variadic template) تعداد دلخواهی آرگومان قالب را با استفاده از بسته پارامتر (parameter pack) می‌پذیرد. نحو از علامت سه‌نقطه (...) استفاده می‌کند. دو نوع بسته وجود دارد:

1. بسته پارامتر قالب — template — صفر یا بیشتر آرگومان نوع را ثبت می‌کند.
2. بسته پارامتر تابع — void f(Ts... args) — آرگومان‌های تابع مربوطه را ثبت می‌کند.

nمی‌توانید مستقیماً روی یک بسته در حلقه تکرار کنید. به‌جای آن، با استفاده از عملگر ... آن را بسط (expand) می‌دهید. نکته کلیدی این است که بسط بسته یک الگو را روی هر عنصر بسته اعمال کرده و یک لیست جداشده با ویرگول تولید می‌کند.

sizeof...(Ts) و sizeof...(args) تعداد عناصر بسته را به‌عنوان constexpr std::size_t برمی‌گردانند.

بسط بازگشتی بسته (الگوی قدیمی)

پیش از عبارات تا در ‎C++‎17، روش استاندارد پردازش بسته‌ها نمونه‌سازی بازگشتی بود: عنصر اول را جدا کنید، پردازش کنید و روی بقیه بازگشت کنید. این نیاز به یک حالت پایه برای پایان بازگشت دارد.

recursive_variadic.cpp
#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
}

عبارات تا — Fold Expressions (‎C++‎17)

عبارات تا (fold expressions) در ‎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) — شامل یک مقدار اولیه

fold_expressions.cpp
#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 ایندکس‌گذاری زمان کامپایل در تاپل‌ها را امکان‌پذیر می‌کند.

tuple_apply.cpp
#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)...) تابع transform را روی هر عنصر فراخوانی می‌کند
  • عبارات تا (‎C++‎17) جایگزین بسط بازگشتی هستند: (... + args) تمام عناصر را جمع می‌زند
  • از تای دوتایی با مقدار اولیه برای مدیریت ایمن بسته‌های خالی استفاده کنید
  • std::apply تاپل را به‌عنوان آرگومان‌های تابع باز می‌کند؛ std::index_sequence ایندکس‌گذاری زمان کامپایل را ممکن می‌سازد

آزمون — دانش خود را بسنجید

(20 امتیاز)

1. `(... + args)` برای `args = {a, b, c}` به چه چیزی بسط می‌یابد؟

2. تفاوت بین `f(args...)` و `f(args)...` چیست؟

3. `sizeof...(args)` چه چیزی برمی‌گرداند؟