Features


Selectable Default Constructor Arguments

by Glen Deen


When Life Gets Complicated

Do you ever create classes that need to initialize lots of data members? You will probably find those constructors growing long, unwieldy parameter lists — how else are you going to specify what to put in each member? Every time you instantiate such an object you will just have to knuckle down and fill in all the arguments. As the number of parameters grows, so does the chance for error, and tedium is guaranteed. Using default parameters can help somewhat, but only in a haphazard sort of way. That's because if any default argument is explicitly specified (i.e., not omitted from the constructor argument list), then all preceding default arguments in the list must also be explicitly specified, whether or not their initial values are the default values.

C++ does provide some ways around this problem, however. In this article I present a couple of ways to avoid the "infinite" argument list, and to selectively initialize just the data members you want, to non-default values. These methods aren't suitable for all programs, but can be very useful for some — especially numerical and scientific programs that involve lots of variables that require initial values.

To keep this discussion simple, I use examples with a small number of default arguments. I admit that in the real world, if the list were short, this problem would be trivial. I ask you to imagine (with my help) the situation when the number of arguments is much larger. Also, in the real world, you may have constructor parameters that have no default values. If so, they always come before the defaultable arguments in the argument list. My examples don't have any.

A Non-Solution

Before I get started, I want to dispense with what may seem like an obvious solution, but really isn't: to reorder N default parameters so that the ones you want to override are at the beginning of the list. To do this, you must write N! overloaded constructors, with each one listing the arguments in a different sequence. This method will work, but with some severe limitations. If you don't do it right, it can take more than n! contractors to avoid ambiguity. It's really not practical, and I won't go into all the reasons here. Suffice it to say that with this technique, using a mere five default parameters will require you to write 120 overloaded constructors!

A Brute-Force Solution

Here's another obvious method, which doesn't require N! constructors. It has an important disadvantage, which I'll explain, but it contains the seed for the Improved Brute-Force Method and its offspring, both of which overcome that disadvantage. The idea is to use a set of overloaded member functions, let's call them arg(...), each of which changes a particular data member and has a void return type. These member functions do the same job as the default constructor arguments, but you can call an arbitrary number of them in any order. If you have N defaultable data members, you need one constructor and N overloaded instances of the arg function. The arg member functions can be overloaded if each parameter is a unique type.

A serious disadvantage of this method is that it does not allow for construction of temporary objects. With this technique, satisfactory construction and initialization takes more than one statement: (1) the object declaration or definition followed by (2) one or more arg calls, each requiring a separate statement. Also, the object must have a name to go in front of .arg(whatever), and temporary objects don't have names.

So the Brute Force Method won't let you construct an object on the fly, as for example within some expression or in a function call argument list. This method also requires each defaultable parameter to be of a different type. Later on, I will show how to disambiguate overloaded constructors when two or more default parameters are of the same type.

Educating the Brute

The Brute-Force Method served as inspiration for the Improved Brute-Force Method: If we change the return values for the arg member functions from void to a reference to the object, we can chain arg calls to an object. This allows defining an object in one line, like this:

Shirt object = 
    Shirt().arg( nsa1 )
           .arg( nsa2 )
               ... ;

where "nsa" means non-standard argument. The form of this definition is called declaration-assignment-initialization.

This chaining of arg calls works because each arg call returns a reference to the (updated) object which then becomes the target for the next arg's member modification operation. (The same principle is used by the ostream insertion operator<< and the assignment operator=.)

The code for the Improved Brute-Force Method is shown in Listing 1. We don't need default constructor arguments, although it would do no harm to use them.

This method essentially solves the problem of overriding just a few of many default constructor arguments with a fairly small amount of code. It also overcomes the disadvantage of the unimproved version because it can construct temporary objects. This method still has the disadvantage of requiring unique defaultable parameter types, however.

It might be more appropriate to call this method the Chained, Overloaded Member Function Member Reinitialization Technique. ( But then again, if I called it that, no one would remember it.) This technique allows you to initialize an arbitrary number of data members in any order with chained calls to the overloaded arg member function.

One thing to be aware of: you can't chain declarations. While you can do this:

// okay
Shirts7=Shirt(blue).arg(tall);

you cannot get away with this:

Shirt s8.arg( shortsleeve );    // Compiler chokes
Shirt s9( blue ).arg( tall );   // Nope -- won't work

While the above three statements appear to be similar, the last two are quite different from the first.

The member selection operators . and -> can only appear in expressions, not in declarations. The latter two statements above are declarations because they begin with a class name. Member selection makes no sense in a declaration.

The Final Refinement

The final refinement is to replace the overloaded arg member functions of the Improved Brute Force Method with overloaded function-call operator()() member functions. This essentially eliminates .arg in the constructor/reinitialization statements. Otherwise, this technique works the same as the Improved Brute Force Method.

There are some drawbacks, of course: use of the function call operator obscures the code because the bare bowlegs contain no information about what they do. At least a member function can be named to give some hint as to what it does. Also, you may prefer to use the operator()() function for another purpose. So, it's your choice.

If you have N defaultable members, you need N overloaded operator()() functions plus either (1) a default constructor or (2) N overloaded one-argument constructors, only one of which has a default argument. Implementing the Shirt class (Listing 1) this way (using option (2)), you can initialize objects as follows:


// default construction
Shirt wrl;
// one-argument construction
Shirt brl( blue );
// declaration-initialization
Shirt ytl = Shirt( yellow )( tall );
// same as ytl above
Shirt tyl = Shirt( tall )( yellow );
Shirt yrs = Shirt( yellow )( shortsleeve );
Shirt sbt = Shirt( shortsleeve )( big )( tan );

I prefer option (2). If your class has lots of members, you might choose option (1). If you do, your only penalty will be that you can't name an argument inside the constructor call. So, the overloaded constructor count becomes N + 1 for option (1), and 2N for option (2). Compare that with N! constructors in the first "solution."

Disambiguating Types

All the techniques discussed so far will not work if two or more of the defaultable members are of the same type. This situation is most likely to arise when the members are builtin types, such as int, float, char, etc.

One way to disambiguate duplicate types is to use two-argument constructors and operator()() functions, in which the first argument is a parameter-specifying enum variable, and the second is the defaultable parameter. For example:

// parameter specifier
enum sleeve_arg { sleeve };
// parameter specifier
enum neck_arg   { neck };
...
// constructor
Shirt::Shirt( neck_arg, float x ) { ... }
// constructor
Shirt::Shirt( sleeve_arg, float x ) { ... }
...
// means neck_arg = 14.5
Shirt neck_object( neck, 14.5 );
// means sleeve_arg = 30
Shirt sleeve_object( sleeve, 30 );

The specifier enum uses its type name, such as sleeve_arg to disambiguate function signatures having the same (or implicitly convertible) types as second arguments.

There is a possible pitfall, however: the object doesn't know when it is completely constructed. If this is important, you could add a special "end" enum type argument which might signal construction completion.

An Example

To illustrate this technique, I made up a larger Shirt class with three enum members and two builtin (float) members, all five of which can have default values. (Never mind that a default neck size and a default sleeve length might not make much sense in the real world.) This class does nothing but get constructed and print out a readable representation of itself via the ostream's operator<<. The Shirt class header file is presented in Listing 2.

The header declares five constructors, one of which has a default argument. Any constructor can occupy the first place in an object construction chain. The "style" constructor will initialize the sleeve length to 0 instead of the default value (dfsleeve) if the specified style is shortsleeve. There are five overloaded operator()() functions, one for each data member. These member functions modify their matching data members and return a reference to the class. The enum_value(size_t i) member function has nothing to do with the subject of this article. I use it only to help display the enum variable values in the test program.

Listing 3 shows the Shirt class implementation. The style and sleeve operator()() functions couple the style to the sleeve length. A sleeve length less than 15 inches is regarded as a shortsleeve. The operator<< function displays a comma-separated list of the data member values. To keep the items lined up in columns, a variable number of trailing spaces is output following each comma. If the style is longsleeve, the sleeve length is output, otherwise "shortsleeve" is output in the same place.

The test program appears in Listing 4. I borrowed a macro idea from Bruce Eckel [1] for testing the Shirt class. The argument a of the macro D(a) is an executable output statement. The macro stringizes this argument and inserts it into the output stream out, and then it executes the statement.

The test program does not instantiate any permanent objects of class Shirt. If it did, it would need to use the declaration-assignment-initialization form to define an object in one line of code. The test program only instantiates temporary objects which are immediately used as arguments to the insertion operator for the ofstream object named out.

The output data from the test program appears at the bottom of Listing 4. In every case, the unspecified arguments are initialized to their default values.

Summary

Long lists of many default constructor arguments can be awkward, especially if only a few of them need overriding for any particular instantiation and some of those are at the end of the list. The Chained, Overloaded Function-Call operator()() Member Reinitialization Technique illustrated here (memorize this name) gives a useful alternative. This technique's syntax allows us to redefine an arbitrary subset of N default constructor arguments in any order without having to define N! constructors.

References

[1] Bruce Eckel. Thinking in C++ (Prentice Hall, 1995), page 238.

Glen Deen has a BSEE degree from The University of Texas. He is the publisher of MicroSky(tm), a microfiche reproduction of the Palomar Observatory Sky Survey. He uses C++ to process astronomical catalogues and to plot star charts. His internet address is glen@metronet.com.