C++11 introduced a variadic template that can accept any number of template arguments, including none. A variadic template is a compile-time mechanism that is instantiated when used.
We recognize a variadic template by the ellipsis ... in the
parameter list:
#include <iostream>
using namespace std;
template <typename... P>
void foo()
{
  cout << __PRETTY_FUNCTION__ << endl;
  cout << sizeof...(P) << endl;
}
int
main()
{
  foo<>();
  foo<int>();
  foo<bool, double>();
}
In the above example, the ellipsis was used in the definition of a parameter pack, and then in the pack expansion.
We define a parameter pack with the ellipsis that is followed by
the pack name, e.g., p.  Pack p is made of parameters
p1, p2, …, p(n-1), pn.
A parameter pack can be of a template or a function.  A parameter pack
is used in a pack expansion or a fold expression.
A template parameter pack defines template parameters of the same kind: in the example above they are of the type kind, in the example below of the value kind.
#include <iostream>
using namespace std;
template <unsigned... N>
void foo()
{
  cout << __PRETTY_FUNCTION__ << endl;
  cout << sizeof...(N) << endl;
}
int
main()
{
  foo<>();
  foo<1>();
  foo<1, 2>();
  // foo<1, -1>();
}
The name of a pack with a trailing ellipsis is called a pack expansion. A template parameter pack is expended to a comma-separated list of template parameters.
#include <iostream>
#include <string>
struct A
{
};
template <typename... P>
struct B: P...
{
  B()
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
};
int
main()
{
  // Expands to a structure type that does not have a base class.
  B<>();
  // Expands to a structure type that has the base class A.
  B<A>();
  // Expands to a structure type derived from A and string.
  B<A, std::string>();
}
In the examples above, the template arguments were explicitly given, because the functions and the constructor didn’t have parameters defined and call arguments passed, based on which the template arguments could be deduced. Arguments for template parameters of a pack are deduced based on the definition of a function parameter pack and the call arguments.
A function parameter pack is defined in the list of function parameters, also with the ellipsis: first we put the name of a template parameter pack, the ellipsis, and then the name of the function parameter pack. Interestingly, the definition of the function parameter pack is also the expansion of the template parameter pack.
#include <iostream>
using namespace std;
template <typename... P>
void foo(P... p)
{
  cout << __PRETTY_FUNCTION__ << endl;
}
int
main()
{
  foo();
  foo(1);
  foo(1, 2.0);
}
In the example above, the function accepts arguments by value.  To
make the function accept its arguments by a const reference, we define
the pack as const Args &... args.
An expansion of a function parameter pack is written as the name of the pack followed by the ellipsis.
#include <iostream>
#include <string>
#include <vector>
template <typename T, typename... P>
auto
factory(P... p)
{
  return T{p...};
}
int
main()
{
  std::cout << factory<std::string>("Hello!") << std::endl;
  auto p = factory<std::vector<int>>(1, 2, 3);
}
A template parameter pack can be expanded in lockstep with (in tandem with) the expansion of the function parameter pack, e.g., when we initialize base objects using parameters of a constructor of the derived class:
#include <iostream>
#include <string>
struct A
{
  A() = default;
  A(int) {}
};
template <typename... P>
struct B: P...
{
  B(const P &... p): P(p)...
  {
    std::cout << __PRETTY_FUNCTION__ << std::endl;
  }
};
int
main()
{
  B<>();
  B<A>(1);
  B<A, std::string>({}, "Hello!");
}
We can process function arguments recursively. The trick is to define two function parameters: the first is a regular one to be processed by the current call, the other is a parameter pack to be processed by recursive call. A pack is expended to an argument list for the recursive call. This way, the recursive-call pack is missing the first parameter of the current-call pack.
#include <iostream>
#include <string>
template <typename P1, typename... P>
void
print(P1 p1, P... p)
{
  std::cout << p1;
  if constexpr(sizeof...(P))
    print(p...);
}
int
main()
{
  print("Hello", ' ', std::string("World"), " x ", 100, '\n');
}
A fold expression (since C++17) describes how to generate a target
(intended) expression using any binary operator op and a
parameter pack.  A fold expression is called that way, because it
folds (like a tablecloth) the target expression (which we could write
“by hand”) to a compressed format.  A fold expression is expanded,
instantiated for a given parameter pack, and that can replace
recursive processing.  We recognize the fold expression by the
ellipsis and parentheses.  There are four versions: two unary, and two
binary, that use the same binary (that requires two operands) operator
op.
Part of a fold expression is expression E that uses pack p.  A
fold expression is expanded by instantating expression E for the
subsequent parameters of pack p.  Expression E for parameter
pi is denoted by Ei.  We denote expression E
instantiated for parameter pi by Ei.
The unary versions require one expression E and operator op.  They
are expanded something like this:
E1 op E2 op … op E(n-1) op En
The result of the above expression depends on the associativity of
operator op, because the direction in which subexpressions with
operator op (e.g., E1 op E2) are evaluated is
given by the associativity of operator op: either from left to right
for the left-to-right associativity, or from right to left for the
right-to-left associativity.
For an associative operation (e.g, addition), the direction doesn’t
matter as the result is the same.  If the operation is not
associative, the direction matters.  Check this out: 3 - 2 - 1 is
evaluated from left to right: (3 - 2) - 1 = 0, and not from right to
left: 3 - (2 - 1) = 2.  Conclusion: operator - must have the
left-to-right associativity (even though it’s not associative).
There is no fold expression that would be expanded as show above, so
that a compiler evaluates the subexpressions in the direction given by
the associativity of operator op.  There are, however, two versions
(of a unary fold expression) that impose the direction:
the left version: (... op E) expended to ((E1 op
E2) op …)
the right version: (E op ...) expended to (… op
(E(n-1) op En))
Therefore:
the left version evaluates the subexpressions from left to right, as
though the operator op had the left-to-right associativity,
the right version evaluates the subexpressions from right to left,
as though the operator op has the right-to-left associativity.
For an associative operation, both versions yield the same result.
However, if an operation is not associative, then we have to choose
the proper version, depending on the associativity of operator op.
In the following example, subtraction is not associative and has the
left-to-right associativity, so we have to use the left version of the
unary fold expression.
template <int... P>
constexpr int left()
{
  return (... - P);
}
template <int... P>
constexpr int right()
{
  return (P - ...);
}
int main()
{
  // (3 - 2) - 1 = 0
  static_assert(left<3, 2, 1>() == 0);
  // 3 - (2 - 1) = 2
  static_assert(right<3, 2, 1>() == 2);
}
In the example below we have to use the right version:
#include <cassert>
template <typename... T>
void left(T &... p)
{
  (... += p);
}
template <typename... T>
void right(T &... p)
{
  (p += ...);
}
int main()
{
  int x = 1, y = 2, z = 3;
  // We would like to inrement y by z, and then x by y:
  //
  // x += y += z;
  //
  // The above compiles and works as expected, because += has the
  // right associativity, and so we need the right fold expression.
  // left(x, y, z);
  right(x, y, z);
  assert(x == 6);
  assert(y == 5);
  assert(z == 3);
}
The binary versions require an initializing expression A that is the
second argument.  A compiler differentiates arguments A and E by
the parameter pack.  There are two versions:
the left version: (A op ... op E) expanded to ((A op
E1) op …)
the right version: (E op ... op A) expanded to (… op
(En op A))
Input-output streams are commonly the initializing expression of a
binary fold expression with the stream insertion (<<) or extraction
(>>) operators, for which we have to use the left version because:
the left operand of a stream operator (>> or <<) is an input or
output stream,
a stream operator returns the stream that it accepted as its left operand,
a stream returned by a stream operator becomes the left operand of another stream operator.
Here’s an example:
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
using namespace std;
template <typename... P>
void
read(P &... p)
{
  (cin >> ... >> p);
  // (p >> ... >> cin);
}
template <typename... P>
void
write(P &&... p)
{
  (cout << ... << p);
  // (p << ... << cout);
}
int
main()
{
  write("Hello", ' ', std::string("World"), " x ", 100, "!\n");
  string txt;
  int x;
  bool b;
  read(txt, x, b);
  write(txt, ' ', x, ' ', b, '\n');
}
Here’s an example with the right version:
#include <cassert>
template <typename R, typename... T>
void left(R &r, T &... p)
{
  (r += ... += p);
}
template <typename R, typename... T>
void right(R &r, T &... p)
{
  (p += ... += r);
}
int main()
{
  int x = 1, y = 2, z = 3;
  // We would like to inrement y by z, and then x by y:
  //
  // x += y += z;
  //
  // The above compiles and works as expected, because += has the
  // right associativity, and so we need the right fold expression.
  // left(x, y, z);
  right(x, y, z);
  assert(x == 6);
  assert(y == 5);
  assert(z == 3);
}
A binary fold expression is handy and expressive, but we could do
without it: we can add expression A to the parameter pack and use
the unary fold expression.  We add either at the front of the pack to
use the left version or at the back to use the right version.  The
example below uses the stream operators, so we have to use the left
version.
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
using namespace std;
template <typename... P>
void
read(P &&... p)
{
  (... >> p);
}
template <typename... P>
void
write(P &&... p)
{
  (... << p);
}
int
main()
{
  write(cout, "Hello", ' ', std::string("World"), " x ", 100, "!\n");
  istringstream in("Hi! 100 0");
  string txt;
  bool b;
  // We don't care about the 100, so we read it into a temporary.
  read(in, txt, int(), b);
  write(cout, txt, ' ', 200, ' ', b, '\n');
}
The binary comma operator is unusual because it joins two
independently-evaluated subexpressions.  The comma has the
left-to-right associativity, so the left subexpression is evaluated
first, unaffected by the parentheses in the right subexpression.
Interestingly, this operator has the lowest priority, yet it
establishes the order in which the subexpressions of higher-priority
operators are evaluated.  In the example below, the comma operators
(and not parentheses) establish the order in which the subexpressions
of operator << are evaluated.
#include <iostream>
using namespace std;
int main()
{
  (cout << '1', cout << '2'), cout << '3';
  cout << '1', (cout << '2', cout << '3');
}
The example below uses the left version, where (std::cout << ", " <<
p) is E, and comma is op.  If pack p is empty, then the fold
expression is empty.  If p has one parameter only, then a compiler
adds an extra empty parameter, if such a one exists (for the comma
operator, it’s void()), because op is binary.  To cater for the
correct printing of a comma, we process the first parameter
separately, outside the pack (as in the recursive processing), and the
pack parameters we process with a fold expression.
#include <iostream>
#include <string>
template <typename T, typename... P>
void
print(const T &t, const P &... p)
{
  std::cout << t;
  (... , (std::cout << ", " << p));
}
int
main()
{
  print(5, "10", std::string("15"));
  std::cout << std::endl;
  // What's this?
  1, void();
  // Well, it's needed here:
  print(1);
  std::cout << std::endl;  
}
A variadic template accepts any number of arguments.
A parameter pack is used in pack expansions or fold expressions.
Variadic can be lists of:
inheritance and initialization,
template parameters and template arguments,
function parameters and function arguments.
Can a template parameter pack accept arguments of different kind?
Can a parameter pack be empty?
What is a fold expression?