Generalized C++ Default Arguments Documentation

Generalized C++ Default Arguments

Better Function Default Arguments in C++ with Template Metaprogramming
by
Thomas Becker
thomas@styleadvisor.com


Contents

  1. Copyright and Permission Notice
  2. Overview
  3. Compatibility
  4. Usage
  5. Overhead

Copyright and Permission Notice

COPYRIGHT (C) 2000 Thomas Becker

PERMISSION 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.

Overview

C++ allows to specify default values for function parameters. However, if a default value is specified for one parameter, then the same must be done for all remaining parameters. Moreover, if a function call uses the default value for a particular parameter, then it necessarily uses the default values for all remaining parameters.

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.

Compatibility

The file default_args.hpp includes files from the Boost type_traits library. Moreover, it is assumed that the compiler supports partial template specialization. Currently, default_args.hpp has been compiled and tested successfully with the Metrowerks compiler version 4.2.5.766 under Windows 2000. It will not compile with Microsoft Visual C++ 6 or 7 due to the lack of suppot for partial template specialization.

Usage

The usage of the generalized default arguments will be explained using an example. From the example, it will be clear how to handle any function and any combination of default arguments.

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.

Implementing a Function

Here's the declaration of Foo:
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.

Calling a Function

If no default values at all are to be used, then the client may use the typedef FooClientParams to package the arguments:
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.

Overhead

As mentioned before, the default argument mechanism is set up in such a way that only one value copy is made of each value parameter. However, an additional reference copy is made of each parameter, regardless of its type. Moreover, the call to the helper function, which is recommended to avoid code bloat, introduces anther reference copy of each argument.

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.