class X; void Foo(int i=42, const X&, int j=43);A caller could then select the default value for any parameter that specifies a default, using the imaginary default keyword, like this:
X anX; Foo(default, anX, 43);I remember several people mentioning casually that this kind of default function argument specification can be "faked" with C++ metaprogramming. However, I have never seen an actual implementation. Therefore, this will be my real-life application of metaprogramming techniques. To be honest, I should say that there is probably a reason why I have never seen this implemented before: the whole thing works nicely, but it is not exactly painless to use. You have to really want these generalized default arguments before you would actually use this. But then, it all depends.
class default_parameter_tag{};
template<
typename ArgType1,
typename ArgType3>
void Foo(ArgType1 arg1,
const X& arg2, ArgType3 arg3)
{
int arg1_processed;
if(ArgType1 converts to int)
arg1_processed = arg1;
else if(ArgType1 ==
default_parameter_tag)
arg1_processed = 42;
else
assert(false);
// similarly for arg3
// ...
}
The selection of the correct if branch should of course happen at compile time, and the assert should be a compile error. Conceptually, this is exactly how our implementation will work: the types of the arguments for which there are default values will be template parameters. When the client passes a dummy object of type default_parameter_tag as an argument, our mechanism will use that type information to detect that the default value should be used for the respective function argument.There is just one more thing to consider before we can tackle the implementation: in order to get a generic solution that works smoothly for any number of function parameters and every possible combination of default values, we must package the function arguments into some sort of structure that can be generated at compile time. In my last column, I showed you exactly this kind of structure as an application of typelists. I also mentioned that Boost has a much more mature implementation of the same idea, namely Jaakko Järvi's tuple class [3]. Basically, this is what we want to use here. However, in order to incorporate the default argument processing seamlessly into the tuple manipulation, I have written my own tuple class for this special purpose.
Let us now look at how the class parameter_tuple is used to provide us with generalized function parameters. To this end, we'll use the function Foo that we declared earlier in imaginary C++ like this:
class X; void Foo(int i=42, const X&, int j=43);To really get the intended default arguments, we now declare Foo like this:
template<typename ParameterTuple> void Foo(const ParameterTuple& args);In fact, any function that wishes to use our generalized default parameters must be declared exactly like this. Therefore, it is necessary to document the actual, intended types of the parameters with the declaration. The best way to do this is to provide a typedef for the correct type of parameter_tuple.
typedef make_parameter_tuple_type< int, const X&, int >::ret FooParams;Listing 1 has the definition of the MetaC++ function make_parameter_tuple_type, which creates an instantiation of the class template parameter_tuple with the indicated element types.
Let us now look at how a client would call the function Foo. If no default values at all are to be used, then the client may use the typedef FooParams to package the arguments:
X anX; Foo(FooParams(52, anX, 53));Notice how we are using the first parameter_tuple constructor here, which takes as its arguments the elements that the tuple is to hold. 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 FooParams and then placing 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_parameter_tuple_type< default_parameter_tag, const X&, int >::ret FooParamsDefault1; X anX; Foo(FooParamsDefault1( default_parameter_tag(), anX, 53 ) );The typedef FooParamsDefault1 could be provided by the implementer of Foo, or it could be left to the client to figure it out. Either way, it's not exactly as pretty as you might wish. It would of course be much nicer if we could create the parameter_tuple using one of those little make functions that abound in the STL, using function template argument deduction to avoid having to explicitly name the template arguments. However, that would not work here because function template argument deduction would fail to deduce reference types like the const X& in our example.
Now let's look at the implementation of Foo in Listing 2. The first two lines of the function definition introduce the default values for the first and third parameters. To this end, another instantiation of the class template parameter_tuple is introduced via the typedef FooDefaultParams. This instantiation is obtained from the original FooParams by inserting the type mandatory_parameter_tag in each argument slot where no default value will be provided. In the second line of Foo's definition, an object named default_args of type FooDefaultParams is declared and filled with the default values for the respective arguments. Dummy objects of type mandatory_parameter_tag are used where no default values are provided.
The crucial part is the line:
FooParams args (client_args, default_args);Here, the final argument tuple for Foo gets constructed, and here is where a MetaC++ program gets executed to decide at compile time if and when default values for the arguments are to be used. Look at the definition of parameter_tuple in Listing 1 and find the constructor from two parameter_tuple objects. First, notice that the constructor works recursively: it first constructs the element of the tuple, and then it recursively constructs the nested tuple. Next, notice that there is a default version of the constructor, and there is an overload for the case where the element type of the first argument is default_parameter_tag [4]. Therefore, when the compiler creates the nested function calls to the constructor from the source line:
FooParams args (client_args, default_args);it actually executes a meta-if branching on each nesting level. If the argument that the client has passed in the object client_args is of the type default_parameter_tag, then the second version of the constructor gets called, which means that the default value is used for that argument. Otherwise, the first version of the constructor is called, and the client's argument is used.
At this point in the definition of Foo, the final set of arguments is available in the tuple args. Listing 2 shows a call to an internal helper function FooInternal as the final line. The reason for this is that for each combination of default values and client-supplied values of Foo, Foo's template parameter ParameterTuple 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. In this example, it is clear how the helper function should be declared:
void FooInternal(int, const X&, int);In general, there is an additional subtlety about the helper function. Ideally, the entire default argument mechanism should be set up so that by the time the final function arguments have been put in the parameter_tuple object, only one copy has been made of each argument that is passed by value. The version that I am describing here does not fulfill that requirement: it makes two copies of each value argument, one when the client packages the arguments and another when the implementer creates the tuple containing the final function arguments. The version that you will find on the CUJ website, <www.cuj.com/code>, fixes that, at the cost of making the source code considerably more complex. Either way, if we pass value arguments to the helper function by value again, then another copy will be made. Therefore, the helper function should take all arguments that were originally passed by value as const references, or as plain references if the implementer of the helper function insists on (ab)using value parameters as modifiable local variables.
The version of parameter_tuple that is presented in Listing 1 works for functions with up to four parameters. The places where it needs to be modified to accommodate more parameters are marked in the code with the comment "EXTEND," and it should be clear what the necessary modifications are.
[2] Andrei Alexandrescu. Modern C++ Design (Addison-Wesley, 2001).
[3] <www.boost.org/libs/tuple/doc/ tuple_users_guide.html>
[4] From the point of view of MetaC++, the second version of the constructor can be viewed as a partial specialization that implements an if branching. Technically, however, it is an overload and not a partial specialization. For a complete discussion of this subject, see: Herb Sutter. "Sutter's Mill: Why Not Specialize Function Templates?" C/C++ Users Journal, July 2001.