Better Function Default Arguments in C++ with Template Metaprogramming
by
Thomas Becker
thomas@styleadvisor.com
COPYRIGHT (C) 2000 Thomas BeckerPERMISSION NOTICE:
PERMISSION TO USE, COPY, MODIFY, AND DISTRIBUTE THIS SOFTWARE FOR ANY PURPOSE AND WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT ALL COPIES ARE ACCOMPANIED BY THE COMPLETE MACHINE-READABLE SOURCE CODE, ALL MODIFIED FILES CARRY PROMINENT NOTICES AS TO WHEN AND BY WHOM THEY WERE MODIFIED, THE ABOVE COPYRIGHT NOTICE, THIS PERMISSION NOTICE AND THE NO-WARRANTY NOTICE BELOW APPEAR IN ALL SOURCE FILES AND IN ALL SUPPORTING DOCUMENTATION.
NO-WARRANTY NOTICE:
THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT EXPRESS OR IMPLIED WARRANTY. ALL IMPLIED WARRANTIES OF FITNESS FOR ANY PARTICULAR PURPOSE AND OF MERCHANTABILITY ARE HEREBY DISCLAIMED.
The header file default_args.hpp contains some class templates and C++ template meta-functions that make it possible to specify and use default values for function arguments without the above restrictions.
Suppose X is a class, and you want to write a function with the following signature:
void Foo(X x, int& i, int j);Assume further that you want to provide default values for the first and last parameter. The following two section explain how to implement the function Foo and how to call it.
typedef make_client_parameter_tuple_type< X, int&, int >::ret FooClientParams; // arg1 = X() // arg2 = mandatory // arg3 = 42 template<typename ClientParameterTuple> void Foo(const ClientParameterTuple& args);The typedef FooClientParams serves two purposes: it documents what Foo's parameter types are, and it provides a typedef of a tuple that can be used to package arguments when Foo gets called (see the next section below for details).
The three comment lines document which parameters have default values and what these default values are.
Finally, Foo itself is declared as a function template with one template parameter ClientParameterTuple and one argument of type const ClientParameterTuple&. Since Foo is a function template, there may of coures not be a separate declaration.
Here's the implementation of Foo:
template<typename ClientParameterTuple> void Foo(const ClientParameterTuple& client_args) { typedef make_client_parameter_tuple_type< X, mandatory_parameter_tag, int >::ret FooDefaultParams; typedef make_function_parameter_tuple_type< X, int&, int >::ret FooFunctionParams; FooDefaultParams default_args( X(), mandatory_parameter_tag(), 42 ); FooFunctionParams args(client_args, default_args); FooInternal( args.get_parameter<0>(), args.get_parameter<1>(), args.get_parameter<2>() ); }The typedef FooDefaultParams in the first line is a typedef for a tuple that will hold the default argument values. This typedef is obtained from the typdef FooClientParams above by placing the dummy type mandatory_parameter_tag in those slots where no default value will be provided.
The typedef FooFunctionParams of the second line is a typedef for a tuple that will hold the final argument values. This typedef differs from the typdef FooClientParams above only insofar as the metafunction that generates the type is make_function_parameter_tuple_type rather than make_client_parameter_tuple_type.
The third line defines an tuple object of type FooDefaultParams. The constructor takes as its arguments the values that the tuple will hold. These values are the default arguments. Dummy objects of type mandatory_parameter_tag are placed into those slots where no default value is provided.
The fourth line defines a tuple object of type FooFunctionParams. The constructor takes two arguments: the tuple that the client has passed, and the tuple that holds the default values. The tuple object thus constructed holds the final argument values for Foo.
The last line of Foo's definition is a call to an internal helper function FooInternal. The reason for this is that for each combination of default values and client-supplied values of Foo, Foo's template parameter has a different value. Therefore, the compiler will instantiate a completely separate version of Foo for each of these combinations. To avoid the code bloat that would result from this, the actual body of Foo should be delegated to the helper FooInternal. The helper function should be declared as follows:
void FooInternal(const X&, int&, int);Notice that the value parameter X gets passed to FooInternal by const reference. The reason for this is to avoid additional overhead. The entire default argument mechanism has been set up so that by the time the final function arguments have been put in the tuple args, only one copy has been made of each argument that is passed by value. If we were to pass value arguments to the helpler function by value again, then another copy would be made. Therefore, the helper function should take all arguments that were originally passed by value as const reference arguments, or as non-const references if the implementer of the helper function insists on (ab)using value parameters as modifiable local variables.
X anX; int i = 42; Foo(FooClientParams(anX, i, 43));If default values are to be used for some or all of the parameters that allow default values, then dummy objects of type default_parameter_tag must be passed in the respective locations. For this to be possible, the packaging tuple must have the appropriate type. This type can be obtained by looking at the definition of FooClientParams and then placing in the type default_parameter_tag in each slot where a default value is to be used. For example, the following call to Foo uses the default for the first argument:
typedef make_client_parameter_tuple_type< default_parameter_tag, int&, int >::ret FooParamsDefault1; int i=42; Foo(FooParamsDefault1( default_parameter_tag(), i, 43 ) );Alternatively, all typedefs such as FooParamsDefault1 could be provided by the implementer of Foo.
Most significantly, a value copy is made of each default value, regardless of whether the default gets used or not. This is different from regular C++ default arguments, where default values are always constructed, but copied only if they actually get used.