Those who read the book “Modern Programming in C ++” by Andrei Alexandrescu know that there is an extensive class of tasks (in the field of metaprogramming using templates) when it is necessary to specify a variable (previously unknown) number of arguments when instantiating. Typical examples of such tasks:
- Description of tuples (tuples)
- Description of types like options
- Description of functors (in this case the list of types of arguments depends on the signature of the function)
- Classification of types according to predefined sets
- etc.
In each such task, the exact number of types passed to the corresponding template as arguments is difficult to determine in advance. And, generally speaking, depends on the desires and needs of those who intend to use the corresponding template class.
Within the framework of the current C ++ standard, there is no convenient solution to such problems. Templates can take a strictly defined number of parameters and nothing else. A. Alexandrescu (in the book mentioned above) proposes a general solution based on the so-called. “Type lists”, in which types are represented as a simply linked list, implemented by means of recursive templates. An alternative solution (used, for example, in boost :: variant and boost :: tuple) is to declare a template class with a large number of parameters to which (all but the first) are assigned some default value. Both of these solutions are partial and do not cover the whole range of possible tasks. Therefore, in order to eliminate the shortcomings of existing solutions and simplify the code, the new standard offers C ++ to developers a new option for declaring templates? “Templates with a variable number of parameters” or, in the original, “variadic templates”.
Simple use cases
The declaration of a template with a variable number of parameters is as follows:
template <
typename ... Types>
class VariadicTemplate
{
};
')
Similarly, templates with a variable number of non-type parameters are declared:
template <
int ... Ints>
class VariadicTemplate
{
};
It should be noted here that emulation of this under the current standard is a very nontrivial task (if not to say that it is not feasible).
In addition to template classes, you can also declare template functions with a variable number of parameters. Similar ads might look like this:
template <
typename ... Type>
void printf (
const char * format, Type ... args);
It is obvious that such kind of template parameters (they are called “parameter packages” or “parameters packs”) cannot be used everywhere where ordinary (single) template parameters can be used. It is acceptable to use parameter packages in the following contexts:
- In the enumeration of the base classes of the template (base-specifier-list);
- In the initialization list of data members in the constructor (mem-initializer-list);
- In initialization lists (initializer-list)
- In the parameter lists of other templates (template-argument-list);
- In the specification of exceptions (dynamic-exception-specification);
- In the attribute list (attribute-list);
- In the list of capture lambda functions (capture-list).
Depending on where exactly the parameter package is used, the elements of this package are interpreted accordingly. The use of the parameter package itself is called “pack expansion” (pack expansion), and is recorded in the code as follows:
Types ...
Where Types is the name of the parameter package.
For example, for such a template declaration:
template <
typename ... Types>
class VariadicTemplate
{
};
Possible options for the parameter package disclosure might look like this:
class VariadicTemplate:
public Types ...
// expansion to the list of base classes. 'public types' - pattern{
// ...// Expand to another parameter list. Pattern - Typestypedef OtherVariadicTemplate <Types ...> OtherVT;
// More difficult option. Pattern - Types *typedef OtherVariadicTemplate <Types * ...> SomeOtherVT;
// Expand to function parameter list. The pattern is Types, a args is a new parameter list:void operator () (Types ... arg)
{
// Expand to argument list when calling functionfoo (& args ...);
}
// Expansion in the initialization list in the constructor:VariadicTemplate (): Types () ...
};
The term "pattern" here refers to a piece of code surrounding the name of a parameter package, which will be repeated when the corresponding package is opened. In the above example, if you carry out the expansion of the parameters manually, you get the template instantiation:
/ * ... * / VariadicTemplate <
int ,
char ,
double >
/ * ... * /Will be disclosed as follows:
class VariadicTemplate:
public int ,
public char ,
public double{
// ...typedef OtherVariadicTemplate <
int ,
char ,
double > OtherVT;
typedef OtherVariadicTemplate <
int *,
char *,
double *> SomeOtherVT;
void operator () (
int args1,
char args2,
double args3)
{
foo (& args1, & args2, & args3);
}
VariadicTemplate ():
int (),
char (),
double ()
// obviously, this code will be non-compiled for such a type list};
As a fairly simple example of using templates with a variable number of parameters, we can give an implementation of a functor. This implementation looks like this:
#
include <
iostream >
// Declare a generic version of the template that stores the function pointer. In this case, all the possible types that can come in the template// in the process of instantiation, we pack in the parameter packagetemplate <
typename ... Args>
struct FunctorImpl;
// Specialize the template for a pointer to simple functions. In this case, we indicate that the parameter package contains the type of the returned// values (R) and arguments (Args). From these two parameters (simple and batch), then we form the signature of the functiontemplate <
typename R,
typename ... Args>
struct FunctorImpl <R (Args ...)>
{
// Describes the type of pointer to the function with the desired signature. At the same time, we open the parameter package.typedef R (* FT) (Args ...);
FunctorImpl (FT fn): m_fn (fn) {;}
// Declare a function call operator in such a way that it takes exactly as many parameters as input as arguments// at the stored function type.R
operator () (Args ... args)
{
// Call the function passing it all the received argumentsreturn m_fn (args ...);
}
FT m_fn;
};
// Declare the general dispatcher patterntemplate <
typename FT>
struct Functor:
public FunctorImpl <FT>
{
Functor (): FunctorImpl <FT> (
NULL ) {;}
Functor (FT fn): FunctorImpl <FT> (fn) {;}
};
int plus_fn (
int a,
int b) {
return a + b;}
int minus_fn (
int a,
int b) {
return a - b;}
int increment (
int & a) {
return a ++;}
int main ()
{
Functor <
int (
int ,
int )> plus (plus_fn);
Functor <
int (
int ,
int )> minus (minus_fn);
Functor <
int (
int &)> inc (increment);
std ::
cout << plus (
10 ,
20 ) <<
"" << minus (
10 ,
20 ) <<
std ::
endl ;
int a =
100 ;
std ::
cout << inc (a) <<
"" ;
std ::
cout << a <<
std ::
endl ;
}
The result of executing this code is quite expected:
30 -10
100 101
and the code is simple and straightforward. For comparison, you can see the files with the implementation of boost :: function.
The templates described above are easy to specialize for pointers to member functions:
// Declare specialization of the function container for the pointer to the member function, specifying the same parameter packagetemplate <
typename T,
typename R,
typename ... Args>
struct FunctorImpl <R (T :: *) (Args ...)>
{
typedef R (T :: * FT) (Args ...);
typedef T HostType;
FunctorImpl (FT fn =
NULL , T * obj =
NULL ): m_fn (fn), m_obj (obj) {;}
// Declare two variants of the function call operator - for the case when the functor is used as a "closure", and when the object// for which the method is called, is passed as the first argumentR
operator () (Args ... args)
{
(m_obj -> * m_fn) (args ...);
}
R
operator () (T * obj, Args ... args)
{
(obj -> * m_fn) (args ...);
}
FT m_fn;
T * m_obj;
};
// Declare a closing class that accepts an object in the constructor for which a member function will be called. It looks very simple.template <
typename FT>
struct Closure:
public FunctorImpl <FT>
{
typedef typename FunctorImpl <FT> :: HostType HostType;
Closure (HostType * obj, FT fn): FunctorImpl <FT> (fn, obj) {;}
};
// Useclass A
{
public :
A (
int base =
0 ): m_base (base) {;}
int foo (
int a) {
return a + m_base;}
private :
int m_base;
};
A b1 (
10 ), b2;
Closure <
int (A :: *) (
int )> a_foo (& b1, & A :: foo);
// You may notice that the general implementation of the functor also works correctly with pointers to member functionsFunctor <
int (A :: *) (
int )> b_foo (& A :: foo);
std ::
cout << a_foo (
20 ) <<
"" << b_foo (& b2,
20 ) <<
"" << b_foo (& b1,
20 ) <<
std ::
endl ;
The above example is quite simple and clearly demonstrates the basic features of templates with a variable number of parameters. Analyzing it, you can determine the following general pattern of using templates with a variable number of parameters:
1. The most common template is declared, the last parameter of which is described as a package of parameters. In the example it is
template <
typename ... Args>
struct FunctorImpl;
2. Partial specializations of this template are defined, specifying one or another part of the parameter package. In the given example, this definition
template <
typename R,
typename ... Args>
struct FunctorImpl <R (Args ...)>
3. In some cases, specialization may require taking into account that the parameter package may be empty. This, generally speaking, is permissible.
It should be remembered that in the case of template classes, parameters packed in a package can be specified starting from the head of the package. It is impossible to specify the parameters starting from the tail of the packet (due to the fact that the parameter packet can only close the list of template parameters). With respect to template functions, there is no such limitation.
More complex cases
As noted above, parameter packages can contain not only types, but also non-types. For example:
// Declare a template that accepts a variable number of integers
template <
int ... Nums>
struct NumsPack
{
// Declare a static array whose size is equal to the number of actually passed argumentsstatic int m_nums [
sizeof ... (Nums)];
// And also declare an enumeration that saves the number of elements in the arrayenum {nums_count =
sizeof ... (Nums)};
};
// Initialize the static arraytemplate <
int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums ...};
Verification code:
typedef NumsPack <
10 ,
20 ,
30 ,
40 ,
50 > Nums_5;
std ::
cout << Nums_5 :: nums_count <<
std ::
endl ;
for (
int n =
0 ; n <Nums_5 :: nums_count; ++ n)
std ::
cout << Nums_5 :: m_nums [n] <<
"" ;
std ::
cout <<
std ::
endl ;
prints to the console expected
five
10 20 30 40 50
The sizeof ... (Nums) construction shown in this example is used to get the number of parameters in a package. In it, Nums is the name of the parameter package. Unfortunately, the design of templates with a variable number of parameters is such that it is the only thing that can be done with a parameter package (besides its disclosure). It is impossible to get a parameter from a package by its index, for example, or to perform any more complicated manipulations within the framework of a new draft standard.
When opening packages, you can apply more complex patterns. For example, in the above code, you can do the following replacement:
template <
int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums *
10 ...};
which results in a different sequence on the screen:
100 200 300 400 500
In general, the specific type of pattern depends on the context in which it is revealed. Moreover, the pattern may contain mention of more than one parameter set. In this case, all the packages mentioned in the pattern will be revealed synchronously, and therefore the number of actual parameters in them must match.
This situation can occur when you need to define tuples of values. Suppose it is necessary to organize a universal functor-composer whose task is to transfer to a certain function the results of the execution of specified functions for a certain argument. Let there be some set of functions:
double fn1 (
double a)
{
return a *
2 ;
}
int fn2 (
int a)
{
return a *
3 ;
}
int fn3 (
int a)
{
return a *
4 ;
}
And two operations:
int test_opr (
int a,
int b)
{
return a + b;
}
int test_opr3 (
int a,
int b,
int c)
{
return a + b * c;
}
It is necessary to write a universal functor, the application of the function call operation to which would lead to the execution of such code:
test_opr(f1(x), f2(x));
or
test_opr3(f1(x), f2(x), f3(x));
The functor must take as input an operation and a list of functions whose results must be passed as arguments to this operation. The framework for defining such a functor may look as follows:
template <
typename Op,
typename ... F>
class Compositor
{
public :
Compositor (Op op, F ... fs);
};
The first problem that needs to be solved is how to save the transferred functions. To do this, you can apply multiple inheritance from classes that directly store data of a specified type:
template <
typename T>
struct DataHolder
{
T m_data;
};
template <
typename Op,
typename ... F>
class Composer:
public DataHolder <F> ...
{
// ...};
But here the first problem arises - if there are several functions in the list of transferred functions that have the same type, then the code will not compile, because the same class will be present in the list of base classes. To eliminate this ambiguity, the types in the package can be indexed. For this, an auxiliary type “tuple of integers” will be used, containing numbers from 0 to the specified as parameter N:
// Determine the class of the actual tupletemplate <
int ... Idxs>
struct IndexesTuple
{
};
// Determine the general view of the template used to generate a tupletemplate <
int Num,
typename Tp = IndexesTuple <>>
struct IndexTupleBuilder;
// Define a specialization that generates a sequence of numbers in the form of a package of integer parameters.// To do this, the second parameter in the template declaration is not the type of the tuple itself, but the previously formed// package. To obtain the final package, we inherit from the generated template, adding a new number to the package.template <
int Num,
int ... Idxs>
struct IndexTupleBuilder <Num, IndexesTuple <Idxs ... >>: IndexTupleBuilder <Num -
1 , IndexesTuple <Idxs ...,
sizeof ... (Idxs) >>
{
};
// Recursion-terminating specialization. Contains a final typedef that defines a tuple with the desired set of numberstemplate <
int ... Idxs>
struct IndexTupleBuilder <
0 , IndexesTuple <Idxs ... >>
{
typedef IndexesTuple <Idxs ...> Indexes;
};
As a result, you can use this template as follows:
typedef typename IndexTupleBuilder <
6 > Indexes;
In this case, Indexes will be equivalent to IndexesTuple <0, 1, 2, 3, 4, 5>
In order for this class to be used in the implementation of the composer, you must enter an intermediate base class, which will inherit from the classes with data. In addition, each class with data will be supplied with its own unique index:
template <
int idx,
typename T>
struct DataHolder
{
DataHolder (T
const & data): m_data (data) {;}
T m_data;
};
// First, declare a generic pattern that accepts a tuple as input. We don’t need to ad directly in this form, but// it is required for subsequent specialization.template <
typename IdxsTuple,
typename ... F>
struct ComposerBase;
// Specialize the general template, retrieving the parameter package from the tuple.// In this case, the template is declared with two parameter packages. This is allowed, because packages can be uniquely separated.// Inheritance uses a pattern in which two parameter packages are mentioned at once. This allows you to uniquely compare// elements of an integer tuple and a list of function types.template <
int ... Idxs,
typename ... F>
struct ComposerBase <IndexesTuple <Idxs ...>, F ...>:
public DataHolder <Idxs, F> ...
{
// And here the pattern contains three packages at once - a package with indices, a package of function types and a package of arguments. All this is revealed in the list// initialize the constructor.ComposerBase (F ... fs): DataHolder <Idxs, F> (fs) ... {;}
};
// Inherit the composer's template from the above template containing the actual datatemplate <
typename Op,
typename ... F>
struct Composer:
public ComposerBase <
typename IndexTupleBuilder <
sizeof ... (F)> :: Indexes, F ...>
{
Op m_op;
public :
// Declare the constructorComposer (Op op, F
const & ... fs): m_op (op), Base (fs ...) {;}
};
In order to complete the composer’s implementation, it is necessary to define a function call operator. For convenience of its definition, the type of the return value is first determined:
template <
typename Op,
typename ... F>
struct Composer:
/ * ... * /{
Op m_op;
public :
typedef decltype (m_op ((* (F *)
NULL ) (
0 ) ...)) result_t;
// ...};
To determine the type of the return value, another new C ++ construction is used - decltype. The result of its use (in this case) is the type of value returned by the function. The design looks a bit strange. It is equivalent in meaning
decltype (op (fs (0) ...))
But since the class fs is not defined in the class scope, the operator is applied to the converted to a reference to the type of the function NULL.
Now everything is ready to define the function call operator. Since the classes that store the functions participating in the composition take an integer index as one of the template parameters, this operator is implemented through an auxiliary function to which all the same integer tuple is passed:
template <
typename Op,
typename ... F>
struct Composer:
/ * ... * /{
Op m_op;
public :
ret_type
operator () (
int x)
const{
return MakeCall (x, Indexes ());
}
private :
// It uses the same trick as in the definition of the ComposerBase class. Tuple type is used to “catch”// package of integer indicestemplate <
int ... Idxs>
ret_type MakeCall (
int x, IndexesTuple <Idxs ...>
const &)
const{
return m_op (DataHolder <Idxs, F> :: m_data (x) ...);
}
};
It remains only to define a function that facilitates the creation of instances of this class:
template <
typename Op,
typename ... F>
Composer <Op, F ...> Compose (Op op, F ... fs)
{
return Composer <Op, F ...> (op, fs ...);
}
and the composer is ready. A couple of examples of its use:
auto f = MakeOp (test_opr, fn1, fn2);
auto ff = MakeOp (test_opr3, fn1, fn2, fn3);
auto ff1 = MakeOp (test_opr3, fn1, fn2, [=] (
int x) {
return f (x) *
5 ;});
// here, the last parameter to the composer is the lambda function.The complete definition of a template composer class is as follows:
template <
int ... Idxs,
typename ... F>
struct ComposerBase <IndexesTuple <Idxs ...>, F ...>:
public DataHolder <Idxs, F> ...
{
ComposerBase (F ... fs): DataHolder <Idxs, F> (fs) ... {;}
};
template <
typename Op,
typename ... F>
struct Composer:
public ComposerBase <
typename IndexTupleBuilder <
sizeof ... (F)> :: Indexes, F ...>
{
Op m_op;
public :
typedef ComposerBase <
typename IndexTupleBuilder <
sizeof ... (F)> :: Indexes, F ...> Base;
typedef decltype (m_op ((* (F *)
NULL ) (
0 ) ...)) result_t;
Composer (Op op, F
const & ... fs): m_op (op), Base (fs ...) {;}
result_t
operator () (
int x)
const{
return MakeCall (x,
typename IndexTupleBuilder <
sizeof ... (F)> :: Indexes ());
}
private :
template <
int ... Idxs>
result_t MakeCall (
int x, IndexesTuple <Idxs ...>
const &)
const{
return m_op (DataHolder <Idxs, F> :: m_data (x) ...);
}
};
Also this class could be implemented on the basis of STL tuples (std :: tuple). In this case, the DataHolder class would not be necessary. In this case, the implementation of the composer will be as follows:
template <
typename Op,
typename ... F>
class TupleComposer
{
Op m_op;
std :: tuple <F ...> m_fs;
public :
typedef decltype (m_op ((* (F *)
NULL ) (
0 ) ...)) result_t;
TupleComposer (Op op, F ... fs): m_op (op), m_fs (fs ...) {;}
result_t
operator () (
int x)
const{
return MakeCall (x,
typename IndexTupleBuilder <
sizeof ... (F)> :: Indexes ());
}
private :
template <
int ... Idxs>
result_t MakeCall (
int x, IndexesTuple <Idxs ...>
const &)
const{
return m_op (
std :: get <Idxs> (m_fs)
(x) ...);
}
};
This option looks a little easier.
Some more tricks
Expansion of the parameter package in the context of the “initialization list” provides the programmer with a considerable freedom of action, since in this case the pattern can be a full-fledged expression. For example, the sum of the numbers passed as arguments can be calculated as follows:
template <
typename ... T>
void ignore (T ...) {;}
template <
typename ... T>
int CalcSum (T ... nums)
{
int ret_val =
0 ;
ignore (ret_val + = nums ...);
return ret_val;}
Check if there are any positive numbers among the transferred numbers - like this:
template <
typename ... T>
bool HasPositives (T ... nums)
{
bool ret_val =
true ;
ignore (ret_val = ret_val && nums> =
0 ...);
return ret_val;}
But when using this method, we must not forget that the sequence of calculations of the arguments, strictly speaking, is not defined, and in what order the operations will be performed - it is impossible to say in advance.
Summarizing, we can say that templates with a variable number of parameters are a very powerful tool that appears in the C ++ language. They are deprived of the obvious shortcomings of the existing types of lists (or other emulations of similar behavior), and allow them to express rather complex concepts with a relatively small amount of code. The constructions given in this article can be compared with similar ones made in the framework of the current standard (for this you can look into the source files boost :: bind, boost :: function, boost :: tuple). But they are not without some flaws. Chief among them is the limited number of contexts in which parameter packages can be expanded. In particular, packages cannot be expanded inside lambda functions (a corresponding request is sent to the standardization committee, but will this request be satisfied?), Packages cannot be expanded into expressions so that you can write, for example, like this:
auto result = args + ...;
package elements cannot be accessed by index.