Variadic templates in C ++ 0x
Those who read the book by Andrei Alexandrescu “Modern C ++ Programming” know that there is an extensive class of tasks (in the field of metaprogramming using templates), when a template needs to specify a variable (previously unknown) number of arguments. Typical examples of such tasks:
- Description of tuples (tuples)
- Description of types like options (variants)
- 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, it depends on the desire and needs of the one who intends to use the corresponding template class.
Under 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) offers a general solution based on the so-called. “Type lists”, in which types are represented as a singly linked list implemented through recursive patterns. An alternative solution (used, for example, in boost :: variant and boost :: tuple) is to declare a template class with a large number of parameters, which (all but the first) have some default value. Both of these solutions are half-hearted and do not cover the whole range of possible tasks. Therefore, To eliminate the shortcomings of existing solutions and simplify the code, the new standard offers C ++ - developers a new option for declaring templates? "Templates with a variable number of parameters" or, in the original, "variadic templates".
A template declaration with a variable number of parameters is as follows:
template < typename ... Types>
class VariadicTemplate
{
};
templates with a variable number of non-type parameters are declared in the same way:
template < int ... Ints>
class VariadicTemplate
{
};
It should be noted here that emulating this in the framework of the current standard is a very non-trivial task (if not to say that it is impossible).
In addition to the 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);
Obviously, this kind of template parameters (they are called “parameter packs” or “parameters packs”) cannot be used wherever regular (single) template parameters can be used. You can use parameter packages in the following contexts:
Depending on where the parameter package is used, the elements of this package are interpreted accordingly. The use of the parameter package itself is called “package expansion”, and is written in the code as follows:
Where Types is the name of the parameter package.
For example, for such a template declaration:
template < typename ... Types>
class VariadicTemplate
{
};
possible options for expanding the package of parameters may look like this:
class VariadicTemplate: public Types ... // expansion to the list of base classes. 'public Types' - pattern
{
// ...
// Expanding to the list of parameters of another template. Pattern - Types
typedef OtherVariadicTemplate <Types ...> OtherVT;
// More complicated option. Pattern - Types *
typedef OtherVariadicTemplate <Types * ...> SomeOtherVT;
// Expanding to the list of function parameters. The pattern is Types, a args is a new list of parameters:
void operator () (Types ... arg)
{
// Expanding to the list of arguments when calling the function
foo (& 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 the package of parameters that will be repeated when the corresponding package is opened. In the above example, if you expand the parameters manually, it turns out what the template instantiation is:
/ * ... * / VariadicTemplate < int , char , double > / * ... * /
It will be expanded 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 turn out uncompiled for such a list of types
};
As a fairly simple example of using templates with a variable number of parameters, we can use the implementation of a functor. This implementation looks like this:
# include < iostream >
// Declare a generic version of a template that stores a pointer to a function. At the same time
, we pack all possible types that can come into the template // into the instantiation process, in the package of parameters
template < typename ... Args> struct FunctorImpl;
// Specialize the template for a pointer to simple functions. At the same time, we indicate that the parameter package contains the return type
// values (R) and arguments (Args). From these two parameters (simple and batch), we then form the function signature
template < typename R, typename ... Args>
struct FunctorImpl <R (Args ...)>
{
// We describe the type of the pointer to the function with the desired signature. At the same time, we open the
typedef parameter package R (* FT) (Args ...);
FunctorImpl (FT fn): m_fn (fn) {;}
// We declare the function call operator so that it takes exactly as many parameters as the arguments
// of the stored function type.
R operator () (Args ... args)
{
// We call the function passing all the arguments received to it
return m_fn (args ...);
}
FT m_fn;
};
// Declare a common template dispatcher
template < 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 this code is quite expected:
30 -10
100 101
and the code is simple and straightforward. For comparison, you can see files with the implementation of boost :: function.
The templates described above can be easily specialized for pointers to member functions:
// Declare the specialization of the function container for a pointer to a member function, specifying the same package of parameters
template < 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) {;}
// We 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, passed as the first argument
R 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 closure class that takes 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) {;}
};
// Using
class 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 functions
Functor < int (A :: *) ( int )> b_foo (& A :: foo);
std :: cout << a_foo ( 20 ) << "" << b_foo (& b2, 20 ) << ""20 ) << std :: endl ;
The above example is quite simple and clearly demonstrates the main features of templates with a variable number of parameters. By analyzing it, one can determine the following general scheme for using templates with a variable number of parameters:
1. The most general template is declared, the last parameter of which is described as a package of parameters. In the example, this is
template < typename ... Args> struct FunctorImpl;
2. Partial specializations of this template are determined that specify one or another part of the parameter package. In the above example, this is the definition of
template < typename R, typename ... Args>
struct FunctorImpl <R (Args ...)>
3. In some cases, when specializing, you may need to consider 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 parameters starting from the tail of the package (due to the fact that the parameter package can only close the list of template parameters). There is no such limitation with respect to template functions.
As noted above, parameter packages can contain not only types, but also non-types. For example:
// We declare a template that takes a variable number of integers
template < int ... Nums>
struct NumsPack
{
// We declare a static array whose size is equal to the number of arguments actually passed
static int m_nums [ sizeof ... (Nums)];
// And also declare an enumeration that stores the number of elements in the array
enum {nums_count = sizeof ... (Nums)};
};
// Initialize the static array
template < 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 the expected
5
10 20 30 40 50 to the console
The sizeof ... (Nums) construct shown in this example is used to get the number of parameters in the 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 this is the only thing that can be done with the package of parameters (in addition to its direct disclosure). It is impossible to get a parameter from a package by its index, for example, or to perform any more complex manipulations within the framework of the new standard project.
When opening packages, you can apply more complex patterns. For example, in the above code, you can make the following replacement:
template < int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums * 10 ...};
which will lead to the conclusion on the screen of a different sequence:
100 200 300 400 500
In general, the specific type of pattern depends on the context in which it is opened. Moreover, the pattern may contain reference to more than one package of parameters. In this case, all the packages mentioned in the pattern will be opened synchronously, and therefore the number of actual parameters in them must match.
Such a situation may arise when it is necessary to determine tuples of values. Suppose it is necessary to organize a universal functor-composer whose task is to transfer the results of the execution of the given functions for some argument to some function. Let there be a certain 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:
or The
functor must accept an operation and a list of functions as input, the results of which must be passed as arguments to this operation. The framework for defining such a functor can look like this:
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 given 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 in the list of transferred functions there are several functions whose types coincide, 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, the auxiliary type "tuple of integers" will be used, containing numbers from 0 to the one specified as parameter N:
// Define the class of the tuple itself
template < int ... Idxs> struct IndexesTuple
{
};
// Define the general view of the template used to generate the tuple
template < int Num, typename Tp = IndexesTuple <>>
struct IndexTupleBuilder;
// Define a specialization that generates a sequence of numbers in the form of a packet of integer parameters.
// For this, the tuple type itself, but the previously formed
package // is not used as the second parameter in the template declaration . To get the final package, we inherit from the generated template, adding a new number
template < int Num, int ... Idxs>
struct IndexTupleBuilder <Num, IndexesTuple <Idxs ... >>: IndexTupleBuilder <Num - 1 , IndexesTuple <Idxs .. ., sizeof ... (Idxs) >>
{
};
// Termination of recursion specialization. Contains the resulting typedef defining a tuple with the desired set of numbers
template < 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, it is necessary to introduce an intermediate base class, which will inherit from data classes. In this case, each class with data will be provided 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 template that accepts a tuple as an input. We don’t need the ad directly in this form, but
// it is required for further specialization.
template < typename IdxsTuple, typename ... F> struct ComposerBase;
// We specialize in a general template, extracting a package of parameters from a tuple.
// In this case, the template is declared with two packages of parameters. This is allowed, because packages can be unambiguously divided
// During inheritance, a pattern is used in which two packages of parameters are mentioned at once. This allows us to unambiguously 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 expanded into the
constructor // initialization list .
ComposerBase (F ... fs): DataHolder <Idxs, F> (fs) ... {;}
};
// Inherit the composer template from the template described above containing the actual data
template < typename Op, typename ... F>
struct Composer: public ComposerBase < typename IndexTupleBuilder < sizeof ... (F)> :: Indexes, F ...>
{
Op m_op;
public :
// Declare the
Composer constructor (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 the convenience of its definition, the type of the returned 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 construct new to C ++ is used - decltype. The result of its application (in this case) is the type of the value returned by the function. The design looks a little strange. In terms of meaning, it is equivalent to such
decltype (op (fs (0) ...))
But since the package fs is not defined in the scope of the class, the operator is applied to the NULL function type converted to the reference.
Now everything is ready to determine the function call operator. Since the classes that store the functions involved in the composition take an integer index as one of the template parameters, this operator is implemented through a helper function into which 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 :
// The same trick is used here as in the definition of the ComposerBase class. The tuple type is used to “catch”
// a package of integer indices
template < 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 full definition of the composer template 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 tuples from STL (std :: tuple). In this case, the DataHolder class would not be necessary. In this case, the composer's implementation 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 bit simpler.
Expanding the package of parameters in the context of the “initialization list” provides the programmer with a sufficiently large freedom of action, since in this case a full expression can be a pattern. 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 positive numbers among the transmitted numbers, like this:
template < typename ... T>
boolHasPositives (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 calculation of arguments, strictly speaking, is not defined, and in what order the operations will be performed - it is impossible to say in advance.
To summarize, we can say that templates with a variable number of parameters are a very powerful tool that appears in C ++. They are devoid of obvious shortcomings of type lists currently existing (or other emulations of similar behavior); they allow a relatively small amount of code to express rather complex concepts. The constructions presented in this article can be compared with similar constructions performed within the framework of the current standard (for this you can look at the source files boost :: bind, boost :: function, boost :: tuple). But they are not without some drawbacks. The main one is a limited number of contexts in which parameter packages can be expanded. In particular, packages cannot be opened inside lambda functions (the corresponding request was sent to the standardization committee, but will this request be satisfied?),
auto result = args + ...;
package elements cannot be accessed by index.
- Description of tuples (tuples)
- Description of types like options (variants)
- 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, it depends on the desire and needs of the one who intends to use the corresponding template class.
Under 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) offers a general solution based on the so-called. “Type lists”, in which types are represented as a singly linked list implemented through recursive patterns. An alternative solution (used, for example, in boost :: variant and boost :: tuple) is to declare a template class with a large number of parameters, which (all but the first) have some default value. Both of these solutions are half-hearted and do not cover the whole range of possible tasks. Therefore, To eliminate the shortcomings of existing solutions and simplify the code, the new standard offers C ++ - developers a new option for declaring templates? "Templates with a variable number of parameters" or, in the original, "variadic templates".
Simple use cases
A template declaration with a variable number of parameters is as follows:
template < typename ... Types>
class VariadicTemplate
{
};
templates with a variable number of non-type parameters are declared in the same way:
template < int ... Ints>
class VariadicTemplate
{
};
It should be noted here that emulating this in the framework of the current standard is a very non-trivial task (if not to say that it is impossible).
In addition to the 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);
Obviously, this kind of template parameters (they are called “parameter packs” or “parameters packs”) cannot be used wherever regular (single) template parameters can be used. You can 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 capture list of a lambda function (capture-list).
Depending on where the parameter package is used, the elements of this package are interpreted accordingly. The use of the parameter package itself is called “package expansion”, and is written 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 expanding the package of parameters may look like this:
class VariadicTemplate: public Types ... // expansion to the list of base classes. 'public Types' - pattern
{
// ...
// Expanding to the list of parameters of another template. Pattern - Types
typedef OtherVariadicTemplate <Types ...> OtherVT;
// More complicated option. Pattern - Types *
typedef OtherVariadicTemplate <Types * ...> SomeOtherVT;
// Expanding to the list of function parameters. The pattern is Types, a args is a new list of parameters:
void operator () (Types ... arg)
{
// Expanding to the list of arguments when calling the function
foo (& 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 the package of parameters that will be repeated when the corresponding package is opened. In the above example, if you expand the parameters manually, it turns out what the template instantiation is:
/ * ... * / VariadicTemplate < int , char , double > / * ... * /
It will be expanded 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 turn out uncompiled for such a list of types
};
As a fairly simple example of using templates with a variable number of parameters, we can use the implementation of a functor. This implementation looks like this:
# include < iostream >
// Declare a generic version of a template that stores a pointer to a function. At the same time
, we pack all possible types that can come into the template // into the instantiation process, in the package of parameters
template < typename ... Args> struct FunctorImpl;
// Specialize the template for a pointer to simple functions. At the same time, we indicate that the parameter package contains the return type
// values (R) and arguments (Args). From these two parameters (simple and batch), we then form the function signature
template < typename R, typename ... Args>
struct FunctorImpl <R (Args ...)>
{
// We describe the type of the pointer to the function with the desired signature. At the same time, we open the
typedef parameter package R (* FT) (Args ...);
FunctorImpl (FT fn): m_fn (fn) {;}
// We declare the function call operator so that it takes exactly as many parameters as the arguments
// of the stored function type.
R operator () (Args ... args)
{
// We call the function passing all the arguments received to it
return m_fn (args ...);
}
FT m_fn;
};
// Declare a common template dispatcher
template < 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 this code is quite expected:
30 -10
100 101
and the code is simple and straightforward. For comparison, you can see files with the implementation of boost :: function.
The templates described above can be easily specialized for pointers to member functions:
// Declare the specialization of the function container for a pointer to a member function, specifying the same package of parameters
template < 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) {;}
// We 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, passed as the first argument
R 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 closure class that takes 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) {;}
};
// Using
class 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 functions
Functor < int (A :: *) ( int )> b_foo (& A :: foo);
std :: cout << a_foo ( 20 ) << "" << b_foo (& b2, 20 ) << ""20 ) << std :: endl ;
The above example is quite simple and clearly demonstrates the main features of templates with a variable number of parameters. By analyzing it, one can determine the following general scheme for using templates with a variable number of parameters:
1. The most general template is declared, the last parameter of which is described as a package of parameters. In the example, this is
template < typename ... Args> struct FunctorImpl;
2. Partial specializations of this template are determined that specify one or another part of the parameter package. In the above example, this is the definition of
template < typename R, typename ... Args>
struct FunctorImpl <R (Args ...)>
3. In some cases, when specializing, you may need to consider 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 parameters starting from the tail of the package (due to the fact that the parameter package can only close the list of template parameters). There is no such limitation with respect to template functions.
More complicated cases
As noted above, parameter packages can contain not only types, but also non-types. For example:
// We declare a template that takes a variable number of integers
template < int ... Nums>
struct NumsPack
{
// We declare a static array whose size is equal to the number of arguments actually passed
static int m_nums [ sizeof ... (Nums)];
// And also declare an enumeration that stores the number of elements in the array
enum {nums_count = sizeof ... (Nums)};
};
// Initialize the static array
template < 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 the expected
5
10 20 30 40 50 to the console
The sizeof ... (Nums) construct shown in this example is used to get the number of parameters in the 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 this is the only thing that can be done with the package of parameters (in addition to its direct disclosure). It is impossible to get a parameter from a package by its index, for example, or to perform any more complex manipulations within the framework of the new standard project.
When opening packages, you can apply more complex patterns. For example, in the above code, you can make the following replacement:
template < int ... Nums>
int NumsPack <Nums ...> :: m_nums [] = {Nums * 10 ...};
which will lead to the conclusion on the screen of a different sequence:
100 200 300 400 500
In general, the specific type of pattern depends on the context in which it is opened. Moreover, the pattern may contain reference to more than one package of parameters. In this case, all the packages mentioned in the pattern will be opened synchronously, and therefore the number of actual parameters in them must match.
Such a situation may arise when it is necessary to determine tuples of values. Suppose it is necessary to organize a universal functor-composer whose task is to transfer the results of the execution of the given functions for some argument to some function. Let there be a certain 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 The
test_opr3(f1(x), f2(x), f3(x));
functor must accept an operation and a list of functions as input, the results of which must be passed as arguments to this operation. The framework for defining such a functor can look like this:
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 given 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 in the list of transferred functions there are several functions whose types coincide, 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, the auxiliary type "tuple of integers" will be used, containing numbers from 0 to the one specified as parameter N:
// Define the class of the tuple itself
template < int ... Idxs> struct IndexesTuple
{
};
// Define the general view of the template used to generate the tuple
template < int Num, typename Tp = IndexesTuple <>>
struct IndexTupleBuilder;
// Define a specialization that generates a sequence of numbers in the form of a packet of integer parameters.
// For this, the tuple type itself, but the previously formed
package // is not used as the second parameter in the template declaration . To get the final package, we inherit from the generated template, adding a new number
template < int Num, int ... Idxs>
struct IndexTupleBuilder <Num, IndexesTuple <Idxs ... >>: IndexTupleBuilder <Num - 1 , IndexesTuple <Idxs .. ., sizeof ... (Idxs) >>
{
};
// Termination of recursion specialization. Contains the resulting typedef defining a tuple with the desired set of numbers
template < 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, it is necessary to introduce an intermediate base class, which will inherit from data classes. In this case, each class with data will be provided 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 template that accepts a tuple as an input. We don’t need the ad directly in this form, but
// it is required for further specialization.
template < typename IdxsTuple, typename ... F> struct ComposerBase;
// We specialize in a general template, extracting a package of parameters from a tuple.
// In this case, the template is declared with two packages of parameters. This is allowed, because packages can be unambiguously divided
// During inheritance, a pattern is used in which two packages of parameters are mentioned at once. This allows us to unambiguously 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 expanded into the
constructor // initialization list .
ComposerBase (F ... fs): DataHolder <Idxs, F> (fs) ... {;}
};
// Inherit the composer template from the template described above containing the actual data
template < typename Op, typename ... F>
struct Composer: public ComposerBase < typename IndexTupleBuilder < sizeof ... (F)> :: Indexes, F ...>
{
Op m_op;
public :
// Declare the
Composer constructor (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 the convenience of its definition, the type of the returned 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 construct new to C ++ is used - decltype. The result of its application (in this case) is the type of the value returned by the function. The design looks a little strange. In terms of meaning, it is equivalent to such
decltype (op (fs (0) ...))
But since the package fs is not defined in the scope of the class, the operator is applied to the NULL function type converted to the reference.
Now everything is ready to determine the function call operator. Since the classes that store the functions involved in the composition take an integer index as one of the template parameters, this operator is implemented through a helper function into which 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 :
// The same trick is used here as in the definition of the ComposerBase class. The tuple type is used to “catch”
// a package of integer indices
template < 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 full definition of the composer template 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 tuples from STL (std :: tuple). In this case, the DataHolder class would not be necessary. In this case, the composer's implementation 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 bit simpler.
Some more tricks
Expanding the package of parameters in the context of the “initialization list” provides the programmer with a sufficiently large freedom of action, since in this case a full expression can be a pattern. 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 positive numbers among the transmitted numbers, like this:
template < typename ... T>
boolHasPositives (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 calculation of arguments, strictly speaking, is not defined, and in what order the operations will be performed - it is impossible to say in advance.
To summarize, we can say that templates with a variable number of parameters are a very powerful tool that appears in C ++. They are devoid of obvious shortcomings of type lists currently existing (or other emulations of similar behavior); they allow a relatively small amount of code to express rather complex concepts. The constructions presented in this article can be compared with similar constructions performed within the framework of the current standard (for this you can look at the source files boost :: bind, boost :: function, boost :: tuple). But they are not without some drawbacks. The main one is a limited number of contexts in which parameter packages can be expanded. In particular, packages cannot be opened inside lambda functions (the corresponding request was sent to the standardization committee, but will this request be satisfied?),
auto result = args + ...;
package elements cannot be accessed by index.