Wednesday, September 27, 2023

variadic templates

Overview
In C, functions such as printf and scanf family supports variable number of arguments. The va_list facility enable passing variable length arguments as shown below.
double stddev(int count, ...) 
{
    double sum = 0;
    double sum_sq = 0;
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i) 
    {
        double num = va_arg(args, double);
        sum += num;
        sum_sq += num*num;
    }
    va_end(args);
    return sqrt(sum_sq/count - (sum/count)*(sum/count));
}
 
 stddev(4, 25.0, 27.3, 26.9, 25.7);

However in C++,  variable number of parameters could not be specified in the declaration of the templates classes. Similarly, a template function could not accept variable number of parameters. 

Details
With the variadic templates feature, it's possible to define template based class or function that can have any number (including zero) of parameters. 
To accomplish this, a new kind of parameter called parameter pack is introduced that can singly represent a list of zero or more parameters in the templates definition. A template class or function with at least one parameter pack is called a variadic template.

Parameter packs
A parameter pack is a new type of parameter used in template definition.
A regular class template parameter or function template parameter maps to a single template argument of the instantiated class or function argument in the function definition.  Whereas a parameter pack map to zero or more template argument of the instantiated class or function argument in the function definition.  
In the template definition, a parameter pack parameter is uniquely identified by placing an ellipsis(...) to the left of the parameter name. According to the context where a parameter pack is used, it can be either a template parameter pack or a function parameter pack.

Template parameter pack
A template parameter pack is a template parameter that represents zero or more number of template parameter arguments. Syntactically, a template parameter pack is specified with an ellipsis(...) to its left. As shown in the example below,  Template parameter packs can be type based (A) or non type based (B).  Here various template classes such as a,b,c,d,e,f are instantiated by passing variable number of template arguments to A and B.
//type based parameter pack
template<class...A> struct X{};
//type based parameter pack expansion X<> a; // the parameter list is empty X<int> b; // the parameter list has one item X<int, char, float> c; // the parameter list has three items //non type based parameter pack template<bool...B> struct Y{};
//non type based parameter pack expansion Y<> d; // the parameter list is empty Y<true> e; // the parameter list has one item Y<true, false, true> f; // the parameter list has three items

In the template definition, the template parameter pack must be the final parameter in the template parameter list. 
template<typename U, typename... Ts>    // OK: can deduce U
struct valid;
// template<typename... Ts, typename U> // Error: Ts... not at the end

Function parameter pack
A function parameter pack is a function parameter that represents zero or more function arguments. Syntactically, a function parameter pack is specified with an ellipsis(...) to the left. 
As seen in the example below, in the template function definition, the template parameter A and in the function definition, the parameter argument args represent function parameter pack.
Here various instances of template function func are generated by passing variable number of function arguments to func
//function template definition
template<class...A> void func(A...args) {};

//expansion
func();                  // void func(); 
func(1);                 // void func(int); 
func(1,'x', 1.0f);       // void func(int,char,float);    

In the template definition, the function parameter pack may appear earlier in the list provided that all following parameters can be deduced from the function arguments, or have default arguments.
template<typename... Ts, typename U, typename=void>
void valid(U, Ts...);    // OK: can deduce U
// void valid(Ts..., U); // Can't be used: Ts... is a non-deduced context in this position

valid(1.0, 1, 2, 3); // OK: deduces U as double, Ts as {int, int, int}

If there are more than one function parameter packs, the last function parameter pack is called trailing parameter pack and others are called non trailing parameter pack. The non trailing parameter pack can be deduced only from explicitly specified arguments. otherwise it needs to be empty.
template<class...A, class...B> 
void func(A...arg, B...arg2) {};

//A:(int, int, int), B:(int, int) 
func<int,int,int>(1,2,3,4,5);

//A: empty, B:(int, int)
func(4,5);

The following diagram illustrates the same.
Pack Expansion
parameter pack expansion happens when a pattern is applied to the elements in the parameter packA pattern contains the name of at least one parameter pack, is expanded into zero or more instantiations, where the name of the parameter pack is replaced by each of the elements from the pack, in order. Syntactically a pack expansion is specified by pattern  and an ellipsis(...) to its right.
A pattern can also be an expression based on parameter pack. While expanding, the expression is evaluated and passed as arguments.
In the example below, two template functions f containing parameter pack Us and g containing parameter pack Ts are defined.  
The client calls g with a set of arguments. These are packed into parameter pack Ts. The function g internally calls function f with a pattern &arg where the parameter pack Ts get expanded in &arg...
Here the pattern is evaluated and function parameters are packed into the parameter pack Us at function f.
template<class... Us>
// Us... pargs expand to int* E1, double* E2, const char** E3
void f(Us... pargs) {}
 
template<class... Ts>
// Ts... args expand to int E1, double E2, const char* E3
void g(Ts... args) {
    // &args... expands to &E1, &E2, &E3
f(&args...); // “&args...” is a pack expansion // “&args” is its pattern } g(1, 0.2, "a");

If the names of two parameter packs appear in the same pattern, they are expanded simultaneously, and they must have the same length.
template<class... Args1>
struct zip
{
    template<class... Args2>
    struct with
    {
        typedef tuple<tuple<Args1, Args2>...> type;
// tuple<Args1, Args2>... is the pack expansion
// tuple<Args1, Args2> is the pattern
}; }; typedef zip<short, int>::with<unsigned short, unsigned>::type T1; // tuple<Args1, Args2>... expands to
// tuple<short, unsigned short>, tuple<int, unsigned int>
// T1 is tuple<tuple<short, unsigned short>, tuple<int, unsigned>>
// typedef zip<short>::with<unsigned short, unsigned>::type T2; // error: pack expansion contains parameter packs of different lengths

Pattern expansion Examples
Two parameter packs can be combined  for selective extraction.
template <size_t... Ns>
struct Indexes {};

template<typename... Ts, size_t... Ns>
auto select(const tuple<Ts...>& t, Indexes<Ns...>) -> decltype(make_tuple(get<Ns>(t)...))
{
    return make_tuple(get<Ns>(t)...);
}

//Example:
// construct tuple<string,string,string,string,string,string>
auto nations = make_tuple("russia", "japan", "cuba", "mexico", "india", "germany"); // construct a tuple of ("japan","india")
auto asia = select(nations, Indexes<1, 4>());
// construct a tuple of ("russia","germany")
auto europe = select(nations, Indexes<0, 5>()); // construct a tuple of ("cuba","mexico")
auto spanish = select(nations, Indexes<2, 3>());

If a pack expansion is nested within another pack expansion, the parameter packs that appear inside the innermost pack expansion are expanded by it, and there must be another pack mentioned in the enclosing pack expansion.
template<class ...A>
void f(const A* ...v) {}

template<class... Args>
void g(Args... args)
{
    f(const_cast<const Args*>(&args)...); 
    // const_cast<const Args*>(&args) is the pattern, it expands two packs
    // (Args and args) simultaneously
    // f(const_cast<const E1*>(&X1), const_cast<const E2*>(&X2), const_cast<const E3*>(&X3))

    //Example:
    g(10,20,30)
    //expands as
    // f(const_cast<const int*>(&(10)), const_cast<const int*>(&(20)), const_cast<const int*>(&(30)))
}
 
template<class... Args>
void i(Args... args) {}

template<class... Args>
void h(Args... args)

template<class... Args>
void k(Args... args)
{
    // f(h(args...) + args...); // Nested pack expansion:
    // inner pack expansion is "args...", it is expanded first
    // outer pack expansion is h(E1, E2, E3) + args..., it is expanded
    // second (as h(E1, E2, E3) + E1, h(E1, E2, E3) + E2, h(E1, E2, E3) + E3)

    //Example:
    k(10,20,30)
    //expands as
    // i(h(10, 20, 30) + 10, h(10, 20, 30) + 20, h(10, 20, 30) + 30)  
}

Pattern expansion Lists
The following discusses different lists  that gets generated during parameter pack expansion of a pattern.

Function argument lists
The following shows pattern expansion within the function call. This is called expression list. Notice that how the position of the of pattern args varies during the parameter pack expansion. 
template <typename... Args>
void f(Args ...args) {}

// pattern:args
f(args...); // expands to f(E1, E2, E3)

// pattern:&args
f(&args...); // expands to f(&E1, &E2, &E3)

// pattern:++args
f(n, ++args...);         // expands to f(n, ++E1, ++E2, ++E3);

// pattern:++args
f(++args..., n);         // expands to f(++E1, ++E2, ++E3, n);

// pattern:args
f((args*2)...); // expands to f(E1*2, E2*2, E3*2)

// pattern:args
f(validate(args)...); // expands to f(validate(E1), validate(E2), validate(E3))


initializer_list 
Pattern expansion can happen in an initializer_list. However it does not support mix of different data types in the parameter pack. For example int and floats.
template<typename... Args>
void printlist(Args... args) 
{
    for (const auto& arg:{args...})
        ;
}

//Example:
printlist(1, 2, 3, 4);     //ok
printlist(1, 2.0, 3, 4);   //error
                                                                                                                 
Parenthesized initializers
A pack expansion may appear inside the parentheses of a direct initializer such as constructor. 
class c1(&args...);             // calls Class::Class(&E1, &E2, &E3)
Class c2 = Class(n, ++args...); // calls Class::Class(n, ++E1, ++E2, ++E3); 
                                 
Template argument lists
Pack expansions can be used anywhere in a template argument list, provided the template has the parameters to match the expansion. 
template<class A, class B, class... C>
void func(A arg1, B arg2, C... arg3)
{
    container<A, B, C...> t1; // expands to container<A, B, E1, E2, E3> 
    container<C..., A, B> t2; // expands to container<E1, E2, E3, A, B> 
    container<A, C..., B> t3; // expands to container<A, E1, E2, E3, B> 
}
                                 
Function parameter list
In a function parameter list, if an ellipsis appears in a parameter declaration (whether it names a function parameter pack (as in, Args... args) or not) the parameter declaration is the pattern. 
template<typename... Ts>
void f(Ts...) {}
 
f('a', 1); // Ts... expands to void f(char, int)
f(0.1);    // Ts... expands to void f(double)
                                 
Template parameter list
Pack expansion may appear in a template parameter list
template<typename... T>
struct value_holder
{
    template<T... Values> // expands to a non-type template parameter 
    struct apply {};      // list, such as <int, char, int(&)[5]>
}
                                 
Exception specification list
Pack expansion may appear in an exception specification list
struct A{};
struct B{};
struct C{};

template<class ...Args>
void f(int i) throw (Args ...) 
{
   switch (i) 
   {
      case 0: throw A();
      case 1: throw B();
      default: throw C();
   }
}
 
f<A,B>(0); // Throws A OK
f<A,B>(1);  // Throws B OK
f<A,B>(2);  // Throws C Terminates
                                 
Base specifier list
A pack expansion may designate a base specifier list of classes in a class declaration as shown below where Education struct has Degree... as  base specifier list. Therefore, the constructor needs to use a pack expansion to call the constructors of these bases.
template<class... Degree>
struct Education: public Degree...
{
    Education():Degree()...{}
};

//Example:
struct MBBS  {};
struct MS    {};
struct BTECH {};
struct MTECH {};

Education<BTECH,MTECH> vrao;
Education<MBBS,MS> krao;

Member initializer list
A pack expansion may designate a Member initializer list of classes in a class declaration as shown below where Education struct has Degree(name)... as  base specifier list. Therefore, the constructor needs to use a pack expansion to call the constructors of these bases.
template<class... Degree>
struct Education: public Degree...
{
    Education(string name):Degree(name)...{}
};

//Example:
struct MBBS {MBBS(string name){}};
struct MS {MS(string name){}};
struct BTECH {BTECH(string name){}};
struct MTECH {MTECH(string name){}};
Education<BTECH,MTECH> vrao("vrao");
Education<MBBS,MS> krao("krao");

sizeof... operator
The number of elements in parameter pack can be returned by sizeof... operator.
template<typename... Args>
size_t kount(Args... args) 
{
    return sizeof...(args);
}

//Example:
kount(1, 2, 3, 4);      //returns 4
kount(1, 2.0, "3, 4");  //returns 3
                                                                                                                 
Lambda captures
Pack expansion may appear in the capture clause of a lambda expression.
void print() { cout << endl;}

template <class T, class ...Rest>
void print(T first, Rest ...rest)
{
    cout << first <<  " ";
    print(rest...);
}

template<class... Args>
void f(Args... args)
{
    auto lm = [&, args...] { return print(args...); };
    lm();
}

//Example:
f(1,2);   //outputs: 1 2  same as print(1,2);
f();      //outputs:      same as print();                                                                                                        

Alignment specifier
Pack expansions are allowed in both the lists of types and the lists of expressions used by the keyword alignas. The instantiations are space-separated.
template<class... T>
struct Align
{
    alignas(T...) unsigned char buffer[128];
};
 
Align<int, short> a; // the alignment specifiers after expansion are
                     // alignas(int) alignas(short)
                     // (no comma in between)

Attribute list
Pack expansions are allowed in the lists of attributes, if permitted by the attribute's specification.
template<int... args>
[[vendor::attr(args)...]] void* f();

Parameter pack forwarding
Parameter packs can be forwarded just like other parameters as shown in the example below.
template <typename ...Params>
void f(Params&&... params)
{
    y(std::forward<Params>(params)...);
}

Complete Examples

smartprint in C++
This Example 33,  mimics printf implementation. Since the stream operator << understands underlying data type and manipulators, there is no need to specify data type or formatting options explicitly . e.g., %d %0.3f %s etc. 
This implementation allows free flowing arguments as well as  specifying argument with a manipulators in a tuple as shown below.
smartprint(cout, 
"Citizen\t\t\t:", make_pair(b, make_tuple(boolalpha)), 
"\nheight(meters)\t\t:", make_pair(h, make_tuple(fixed,setprecision(3))),
"\nincome\t\t\t:", make_pair(put_money(m),  make_tuple(showbase)),
"\nSocial Security number\t:", make_pair(ssn, make_tuple(left,setw(8),setfill('#'),showbase)),
"\ntime stamp\t\t:", put_time(&tm, "%c %Z"),
"\n")

Citizen			:true
height(meters)		:1.568
income			:$1,234.56
Social Security number	:123#####
time stamp		:Sat 10 Feb 2024 07:26:34 AM UTC UTC
Extract a value from parameter pack
In this Example 34, the a value is retrieved from a parameter pack. Since heterogenous values cannot be returned by the same function, the address of the element is returned. Later data type of the element is retrieved and returned after casting.
getter<2>(100, 2.34, "3,4")

output:
3,4
Output std::tuple to  a stream (operator <<)
std::tuple introduced in C++11 is similar to std::pair, enables heterogenous collection of one or more data types. This Example 35 contains an implementation using recursion to output to a stream.
tuple<int,float, string> t(2018,4.11, "Khri$ha")
[2018,4.11,Khri$ha]

tuple<int> t2(1975)
[1975]

Tuple implementation
std::tuple introduced in C++11 is similar to std::pair, enables heterogenous collection of one or more data types. This Example 36 contains an implementation of the Tuple class  using recursive parameter packs.

Tuple<int, double, string> t(1, 2.0, "3,4")
get<0>(t) = 100;

get<0>(t):	100
get<1>(t):	2
get<2>(t):	3,4


No comments:

Post a Comment