Inapoi
Inainte
Cuprins
Default arguments
In Stash3.h, examine the two
constructors for Stash( ). They don’t seem all that different,
do they? In fact, the first constructor seems to be a special case of the second
one with the initial size set to zero. It’s a bit of a waste of
effort to create and maintain two different versions of a similar
function.
C++ provides a remedy with default
arguments. A default
argument is a value given in the declaration that the compiler automatically
inserts if you don’t provide a value in the function call. In the
Stash example, we can replace the two functions:
Stash(int size); // Zero quantity
Stash(int size, int initQuantity);
with the single
function:
Stash(int size, int initQuantity = 0);
The Stash(int) definition is
simply removed – all that is necessary is the single Stash(int,
int) definition.
Now, the two object
definitions
Stash A(100), B(100, 0);
will produce exactly the same results.
The identical constructor is called in both cases, but for A, the second
argument is automatically substituted by the compiler when it sees the first
argument is an int and that there is no second argument. The compiler has
seen the default argument, so it knows it can still make the function call if it
substitutes this second argument, which is what you’ve told it to do by
making it a default.
Default arguments are a convenience, as
function overloading is a convenience. Both features allow you to use a single
function name in different situations. The difference is that with default
arguments the compiler is substituting arguments when you don’t want to
put them in yourself. The preceding example is a good place to use default
arguments instead of function overloading; otherwise you end up with two or more
functions that have similar signatures and similar behaviors. If the functions
have very different behaviors, it doesn’t usually make sense to use
default arguments (for that matter, you might want to question whether two
functions with very different behaviors should have the same
name).
There are two rules you must be aware of
when using default arguments. First, only
trailing arguments may be
defaulted. That is, you can’t have a default argument followed by a
non-default argument. Second, once you start using default arguments in a
particular function call, all the subsequent arguments in that function’s
argument list must be defaulted (this follows from the first
rule).
Default arguments are only placed in the
declaration of a function (typically placed in a header
file). The compiler must see the
default value before it can use it. Sometimes people will place the commented
values of the default arguments in the function definition, for documentation
purposes
void fn(int x /* = 0 */) { // ...
Placeholder arguments
Arguments in a function declaration can
be declared without identifiers. When these are used
with default arguments, it can look a bit funny. You can end up
with
void f(int x, int = 0, float = 1.1);
In C++ you don’t need identifiers
in the function definition, either:
void f(int x, int, float flt) { /* ... */ }
In the function body, x and
flt can be referenced, but not the middle argument, because it has no
name. Function calls must still provide a value for the placeholder, though:
f(1) or f(1,2,3.0). This syntax allows you to put the argument in
as a placeholder without using it. The idea is that you might want to change the
function definition to use the placeholder later, without changing all the code
where the function is called. Of course, you can accomplish the same thing by
using a named argument, but if you define the argument for the function body
without using it, most compilers will give you a warning message, assuming
you’ve made a logical error. By intentionally leaving the argument name
out, you suppress this warning.
More important, if you start out using a
function argument and later decide that you don’t need it, you can
effectively remove it without generating warnings, and yet not disturb any
client code that was calling the previous version of the
function.
Choosing overloading vs. default
arguments
Both function overloading and default
arguments provide a convenience for calling function names. However, it can seem
confusing at times to know which technique to use. For example, consider the
following tool that is designed to automatically manage blocks of
memory for you:
//: C07:Mem.h
#ifndef MEM_H
#define MEM_H
typedef unsigned char byte;
class Mem {
byte* mem;
int size;
void ensureMinSize(int minSize);
public:
Mem();
Mem(int sz);
~Mem();
int msize();
byte* pointer();
byte* pointer(int minSize);
};
#endif // MEM_H ///:~
A Mem object holds a block of
bytes and makes sure that you have enough storage. The default
constructor doesn’t allocate any storage, and the second constructor
ensures that there is sz storage in the Mem object. The destructor
releases the storage, msize( ) tells you how many bytes there are
currently in the Mem object, and pointer( ) produces a
pointer to the starting address of the storage (Mem is a fairly low-level
tool). There’s an overloaded version of pointer( ) in which
client programmers can say that they want a pointer to a block of bytes that is
at least minSize large, and the member function ensures
this.
Both the constructor and the
pointer( ) member function use the private
ensureMinSize( ) member function to increase the size of the memory
block (notice that it’s not safe to hold the result of
pointer( ) if the memory is resized).
Here’s the implementation of the
class:
//: C07:Mem.cpp {O}
#include "Mem.h"
#include <cstring>
using namespace std;
Mem::Mem() { mem = 0; size = 0; }
Mem::Mem(int sz) {
mem = 0;
size = 0;
ensureMinSize(sz);
}
Mem::~Mem() { delete []mem; }
int Mem::msize() { return size; }
void Mem::ensureMinSize(int minSize) {
if(size < minSize) {
byte* newmem = new byte[minSize];
memset(newmem + size, 0, minSize - size);
memcpy(newmem, mem, size);
delete []mem;
mem = newmem;
size = minSize;
}
}
byte* Mem::pointer() { return mem; }
byte* Mem::pointer(int minSize) {
ensureMinSize(minSize);
return mem;
} ///:~
You can see that
ensureMinSize( ) is the only function responsible for allocating
memory, and that it is used from the second constructor and the second
overloaded form of pointer( ). Inside ensureMinSize( ),
nothing needs to be done if the size is large enough. If new storage must
be allocated in order to make the block bigger (which is also the case when the
block is of size zero after default construction), the new “extra”
portion is set to zero using the Standard C library
function memset( ), which was introduced in Chapter 5. The
subsequent function call is to the Standard C library function
memcpy( ), which in this case copies the
existing bytes from mem to newmem (typically in an efficient
fashion). Finally, the old memory is deleted and the new memory and sizes are
assigned to the appropriate members.
The Mem class is designed to be
used as a tool within other classes to simplify their memory management (it
could also be used to hide a more sophisticated memory-management system
provided, for example, by the operating system). Appropriately, it is tested
here by creating a simple “string” class:
//: C07:MemTest.cpp
// Testing the Mem class
//{L} Mem
#include "Mem.h"
#include <cstring>
#include <iostream>
using namespace std;
class MyString {
Mem* buf;
public:
MyString();
MyString(char* str);
~MyString();
void concat(char* str);
void print(ostream& os);
};
MyString::MyString() { buf = 0; }
MyString::MyString(char* str) {
buf = new Mem(strlen(str) + 1);
strcpy((char*)buf->pointer(), str);
}
void MyString::concat(char* str) {
if(!buf) buf = new Mem;
strcat((char*)buf->pointer(
buf->msize() + strlen(str) + 1), str);
}
void MyString::print(ostream& os) {
if(!buf) return;
os << buf->pointer() << endl;
}
MyString::~MyString() { delete buf; }
int main() {
MyString s("My test string");
s.print(cout);
s.concat(" some additional stuff");
s.print(cout);
MyString s2;
s2.concat("Using default constructor");
s2.print(cout);
} ///:~
All you can do with this class is to
create a MyString, concatenate text, and print to an
ostream. The class only contains a pointer to a
Mem, but note the distinction between the default constructor, which sets
the pointer to zero, and the second constructor, which creates a Mem and
copies data into it. The advantage of the
default constructor is that you
can create, for example, a large array of empty MyString objects very
cheaply, since the size of each object is only one pointer and the only overhead
of the default constructor is that of assigning to zero. The cost of a
MyString only begins to accrue when you concatenate data; at that point
the Mem object is created if it hasn’t been already. However, if
you use the default constructor and never concatenate any data, the destructor
call is still safe because calling delete for
zero is defined such that it does not try to release storage or otherwise cause
problems.
If you look at these two constructors it
might at first seem like this is a prime candidate for default arguments.
However, if you drop the default constructor and write the remaining constructor
with a default argument:
MyString(char* str = "");
everything will work correctly, but
you’ll lose the previous efficiency benefit since a Mem object will
always be created. To get the efficiency back, you must modify the
constructor:
MyString::MyString(char* str) {
if(!*str) { // Pointing at an empty string
buf = 0;
return;
}
buf = new Mem(strlen(str) + 1);
strcpy((char*)buf->pointer(), str);
}
This means, in effect, that the default
value becomes a flag that causes a separate piece of code to be executed than if
a non-default value is used. Although it seems innocent enough with a small
constructor like this one, in general this practice can cause problems. If you
have to look for the default rather than treating it as an ordinary
value, that should be a clue that you will end up with effectively two different
functions inside a single function body: one version for the normal case and one
for the default. You might as well split it up into two distinct function bodies
and let the compiler do the selection. This results in a slight (but usually
invisible) increase in efficiency, because the extra argument isn’t passed
and the extra code for the conditional isn’t executed. More importantly,
you are keeping the code for two separate functions in two separate
functions rather than combining them into one using default arguments, which
will result in easier maintainability, especially if the functions are
large.
On the other hand, consider the
Mem class. If you look at the definitions of the two constructors and the
two pointer( ) functions, you can see that using default arguments
in both cases will not cause the member function definitions to change at all.
Thus, the class could easily be:
//: C07:Mem2.h
#ifndef MEM2_H
#define MEM2_H
typedef unsigned char byte;
class Mem {
byte* mem;
int size;
void ensureMinSize(int minSize);
public:
Mem(int sz = 0);
~Mem();
int msize();
byte* pointer(int minSize = 0);
};
#endif // MEM2_H ///:~
Notice that a call to
ensureMinSize(0) will always be quite efficient.
Although in both of these cases I based
some of the decision-making process on the issue of
efficiency, you must be careful not to fall into the
trap of thinking only about efficiency (fascinating as it is). The most
important issue in class design is the interface of the class (its public
members, which are available to the client programmer). If these produce a class
that is easy to use and reuse, then you have a success; you can always tune for
efficiency if necessary but the effect of a class that is designed badly because
the programmer is over-focused on efficiency issues can be dire. Your primary
concern should be that the interface makes sense to those who use it and who
read the resulting code. Notice that in MemTest.cpp the usage of
MyString does not change regardless of whether a default constructor is
used or whether the efficiency is high or
low.
Summary
As a guideline, you shouldn’t use a
default argument as a flag upon
which to conditionally execute code. You should instead break the function into
two or more overloaded functions if you can. A default argument should be a
value you would ordinarily put in that position. It’s a value that is more
likely to occur than all the rest, so client programmers can generally ignore it
or use it only if they want to change it from the default
value.
The default argument is included to make
function calls easier, especially when those functions have many arguments with
typical values. Not only is it much easier to write the calls, it’s easier
to read them, especially if the class creator can order the arguments so the
least-modified defaults appear latest in the list.
An especially important use of default
arguments is when you start out with a function with a set of arguments, and
after it’s been used for a while you discover you need to add arguments.
By defaulting all the new arguments, you ensure that all client code using the
previous interface is not disturbed.
Exercises
Solutions to selected exercises
can be found in the electronic document The Thinking in C++ Annotated
Solution Guide, available for a small fee from
www.BruceEckel.com.
- Create a Text class
that contains a string object to hold the text of a file. Give it two
constructors: a default constructor and a constructor that takes a string
argument that is the name of the file to open. When the second constructor is
used, open the file and read the contents into the string member object.
Add a member function contents( ) to return the string so
(for example) it can be printed. In main( ), open a file
using Text and print the
contents.
- Create a
Message class with a constructor that takes a single string with a
default value. Create a private member string, and in the constructor
simply assign the argument string to your internal string. Create
two overloaded member functions called print( ): one that takes no
arguments and simply prints the message stored in the object, and one that takes
a string argument, which it prints in addition to the internal message.
Does it make sense to use this approach instead of the one used for the
constructor?
- Determine
how to generate assembly output with your compiler, and run experiments to
deduce the name-decoration
scheme.
- Create a
class that contains four member functions, with 0, 1, 2, and 3 int
arguments, respectively. Create a main( ) that makes an object of
your class and calls each of the member functions. Now modify the class so it
has instead a single member function with all the arguments defaulted. Does this
change your
main( )?
- Create
a function with two arguments and call it from main( ). Now make one
of the arguments a “placeholder” (no identifier) and see if your
call in main( )
changes.
- Modify
Stash3.h and Stash3.cpp to use default arguments in the
constructor. Test the constructor by making two different versions of a
Stash
object.
- Create a new
version of the Stack class (from Chapter 6) that contains the default
constructor as before, and a second constructor that takes as its arguments an
array of pointers to objects and the size of that array. This constructor should
move through the array and push each pointer onto the Stack. Test your
class with an array of
string.
- Modify
SuperVar so that there are #ifdefs around all the vartype
code as described in the section on enum. Make vartype a regular
and public enumeration (with no instance) and modify print( )
so that it requires a vartype argument to tell it what to
do.
- Implement
Mem2.h and make sure that the modified class still works with
MemTest.cpp.
- Use
class Mem to implement Stash. Note that because the implementation
is private and thus hidden from the client programmer, the test code does
not need to be
modified.
- In
class Mem, add a bool moved( ) member function that
takes the result of a call to pointer( ) and tells you whether the
pointer has moved (due to reallocation). Write a main( ) that tests
your moved( ) member function. Does it make more sense to use
something like moved( ) or to simply call pointer( )
every time you need to access the memory in
Mem?
 |
|