Sari la conținut

Șabloane variadice și expresii pliate

Gestionați orice număr de argumente șablon cu pachete de parametri. Master fold expresii pentru procesare elegantă, fără recursive a pachetelor.

Pachete de parametri

Un șablon variadic acceptă un număr arbitrar de argumente șablon folosind un pachet de parametri. Sintaxa folosește o elipsă (...). Există două tipuri de pachete:

1. Pachetul de parametri șablon —template — captează zero sau mai multe argumente de tip.
2. Pachetul de parametri de funcție —void f(Ts... args)— captează argumentele funcției corespunzătoare.

Nu puteți repeta direct peste un pachet într-o buclă. În schimb, îl extindeți folosind operatorul.... Perspectiva cheie este că extinderea pachetului aplică un model fiecărui element al pachetului și produce o listă separată prin virgulă.

sizeof...(Ts)șisizeof...(args)returnează numărul de elemente din pachet caconstexpr std::size_t.

Expansiune pachet recursiv (model moștenit)

Înainte de a plia expresiile C++17, modalitatea standard de procesare a pachetelor era instanțiarea recursiva: îndepărtați primul element, procesați-l și recurgeți la restul. Acest lucru necesită un caz de bază pentru a termina recursiunea.

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 expresii (C++17)

C++17 expresiile pliate elimină nevoia de șabloane recursive în majoritatea cazurilor. O expresie fold aplică un operator binar peste toate elementele unui pachet de parametri. Există patru forme:

- Unar la dreapta:(pack op ...)se extinde lae1 op (e2 op (... op eN))
- Unar stânga:(... op pack)se extinde la((e1 op e2) op ...) op eN
- Binary right fold:(pack op ... op init)— include o valoare inițială
- Binary left fold:(init op ... op pack)— include o valoare inițială

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';
}

Contexte de extindere a pachetului

Expansiunea pachetului (...) poate apărea în multe contexte, nu doar în argumentele apelurilor de funcție:

- Argumente ale funcției:f(args...)sauf(transform(args)...)
- Argumente șablon:std::tuple
- Clasele de bază:class Derived : public Bases... { };
- Liste de inițializare:auto list = { args... };
- Capturi Lambda:[args...] { }sau[...args = std::move(args)] { }(captură inițială C++20)
- Utilizarea declarațiilor (C++17):using Bases::operator()...;

Modelul este orice apare înaintea.... Înf(transform(args)...), modelul estetransform(args), care se extinde laf(transform(a1), transform(a2), transform(a3)).

Acest lucru este crucial de înțeles:f(args)...nu este același cuf(args...). Primul se extinde laf(a1), f(a2), f(a3)(trei apeluri separate într-o expresie virgulă), în timp ce al doilea se extinde laf(a1, a2, a3)(un apel cu trei argumente).

std::tuple și std::se aplică

std::tupleeste containerul eterogen al bibliotecii standard, construit pe șabloane variadice. std::apply (C++17) apelează un apelabil cu elemente tuple dezambalate ca argumente. std::index_sequence permite indexarea în timp de compilare în tupluri.

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)
}
Capcană

Cele mai frecvente greșeli cu șabloanele variadice:

1. Uitând... — scriereaargscând vrei să spuiargs...lasă pachetul neexpandat, provocând o eroare de compilare.
2. Extinderea în locul greșit —f(g(args)...)vsf(g(args...))au semnificații foarte diferite. Primul apeleazăgo dată pe element; al doilea apeleazăgcu toate elementele.
3. Pachete goale —(... + args)cu un pachet gol este prost format pentru majoritatea operatorilor. Folosiți o îndoire binară cu o valoare inițială:(0 + ... + args).
4. sizeof...returnează numărul, nu dimensiunea în octeți —sizeof...(args)oferă numărul de elemente de pachet, nu dimensiunea lor combinată de memorie.

Bună practică

Preferați expresiile de pliere față de șabloanele variadice recursive ori de câte ori este posibil. Ele sunt mai concise, mai ușor de citit și adesea se compilează mai rapid. Utilizați pliuri binare cu o valoare inițială explicită pentru a gestiona pachetele goale în siguranță. Rezervați expansiunea recursivă pentru cazurile în care expresiile de pliere sunt insuficiente - de exemplu, când trebuie să procesați elemente în perechi sau când procesarea fiecărui element necesită o logică diferită.

Idei cheie
  • Șabloanele variadice folosesctypename... Ts(pachet de parametri șablon) șiTs... args(pachet de parametri de funcție)
  • sizeof...(args)returnează numărul de elemente dintr-un pachet în momentul compilării
  • Expansiunea pachetului aplică un model fiecărui element:f(transform(args)...)apelează la transformarea fiecărui element
  • Expresiile Fold (C++17) înlocuiesc expansiunea recursivă:(... + args)însumează toate elementele
  • Utilizați pliuri binare cu o valoare inițială pentru manipularea în siguranță a pachetelor goale
  • std::applydespachetează un tuplu ca argumente ale funcției;std::index_sequencepermite indexarea în timp de compilare

Test — Verifică-ți cunoștințele

(20 XP)

1. La ce se extinde`(... + args)`pentru`args = {a, b, c}`?

2. Care este diferența dintre`f(args...)`și`f(args)...`?

3. Ce returnează`sizeof...(args)`?