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 which 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);
};
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(
)
where the client programmer 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 earlier in the book. 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 a very 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 very 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 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);
};
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.
Contact: webmaster@codeguru.com
CodeGuru - the website for developers.