C++ (not C) has a convenience feature that allows programmers to specify default parameters on functions when declaring them. The compiler uses the default value if no value is supplied by the caller for that parameter, instead of complaining about a missing parameter value. I am not going to discuss the mechanics of declaring these default parameters; any C++ book will explain this. I’m going to delve into what problems happen over time when using default parameters during software maintenance phase as code ages and changes hands between programmers.
Functions land up having default values in one of several modes of thinking.
When a function performs two related but converse operations, the programmer might decide that providing a default parameter will be a convenience when calling the function. A typical function that is used like this is:
void SaveFile(boolean save=TRUE);
The function can save or load a file from disk based on the supplied parameter. Due to the default, calling the function without a parameter results in a save operation being performed.
This looks good as when using this function, the caller simply says SaveFile() to save the file which reads OK. The problem happens when the statement SaveFile(FALSE) appears in code. In time the statement SaveFile(FALSE) make less sense to the programmer and makes code less readable. Does it mean that the file will not be saved? There is no indication of a load operation being performed anywhere how am I supposed to know? Well I must dig down into the header and look at the function declaration. But unless there are comments in place it could take a while to figure out how the parameter affects the functions operation. Also note that both saving and loading have a 50% chance of occurrence, so the default is not really the most used value but rather a value that suits the function name. If the function was called LoadFile the default value would be save=FALSE. Not much of a convenience really but a headache to maintain as one must look at the functions declaration every time and possible read documentation to understand what to do with that parameter. If you don’t believe me try and remember what CWnd::UpdateData does.
The solution is to drop the default value and provide two separate functions SaveFile() and LoadFile() even if the operations are related. Now you understand what the function does in its entirety without having to look at its declaration.
A Frequently Requested Operation
When a function is usually supposed to perform a certain operation but can occasionally perform an extended but related operation the programmer might decide to default the function parameter to the frequently performed operation.
Take a typical example:
char* DoubleToString(double value, int precision=5);
The function converts a double value to a string. It is possible to specify precision but the programmer knows that most of the time the value will need a precision of 5 digits and defaults the precision to 5, making the precision parameter optional.
Despite this articles purpose being that of trying to distract programmers from using default parameters, I must say that this mode of reasoning for using a default parameter is probably the most correct. It is nevertheless fraught with maintenance problems.
If the programmers assumption is correct that the most often used precision value is 5, then most calls to this function scattered around code will look like:
myString = DoubleToString(myDouble)
The chances are that in time it will be forgotten that this function is actually cleverer and can provide a more or less precise value if desired. The magic of copy and paste will work and unless someone bothers to look at the declaration, the functions advanced operation will be lost to the world. Many a places over precise or under precise values will be shown simply because no one cared to check that there was actually another parameter available to control precision. The programmer’s assumption about the precision being 5 will become 'law in the land' and no one will care about questioning if the default precision is still 5?
The solution is to not provide any default. Leave it to the caller to explicitly specify the precision.
char* DoubleToString(double value, int precision);
Now calls to the function will look like myString = DoubleToString(myDouble, 5) and another programmer looking at it will muse, “hmm what does the 5 mean” and dig down to the header and enlighten himself to the fact that he can control precision. In other words, in this case we want the programmer to dig deeper and go down to the declaration as it will benefit him and expose him to a more advanced API.
A Maintenance Convenience
A third case where I have seen default values come into existence is during maintenance phase rather than design phase. A function with no defaults has been performing an age old operation all this while. Now there is a need to tweak the operation slightly in some cases for newer code. The programmer maintaining code gets the bright idea that he can add a parameter to control whether the tweaked operation must be performed or the original operation. He gets the brighter idea that if he defaults the added parameter to perform the original operation, he will retain old API as is and can get away without having to change all the places the function is originally called from. Only new code will change. The dread of changing API and locating and changing all callers in old code is averted.
A typical example:
void ClearPtrList() // Clear the list, does not delete objects in the list.
void ClearPtrList(boolean DeepDelete=FALSE); // Clear the list // If DeepDelete, delete objects in the list before clearing.
Of all reasons for using default parameters discussed in this article, this is the worst. It is based on fear of changing old code rather than a conscious design decision. The decision of deleting objects in the list must be consciously made by the caller, so we want someone calling ClearPtrList to look at the declaration and decide what to do. Supplying a default means that most callers will go with the default which will not delete objects in the list, simply clear it. And while it may work OK without any issues in 9 of 10 cases, it will cause a memory leak in 1 of 10 cases. What is worse is that all old code will still do a ClearPtrList() and an innocent programmer calling ClearPtrList() (which is what all the old code does, so it must be correct) will have no idea that this call may leak memory in new code. It will take hours of debugging before it is spotted and the call fixed to read ClearPtrList(TRUE). But the root of the problem will still remain, as a different programmer may repeat the same mistake and have to go through the gyration of debugging a leak again – if the leak is ever spotted!
The solution is to drop the default in the declaration and change any old callers to always supply the default. There are no shortcuts to design even when maintaining code.
void ClearPtrList(boolean DeepDelete); // Clear the list // If DeepDelete, delete objects in the list before clearing.
Let us again go back and examine why the default was introduced here. It got introduced because the programmer who modified the ClearPtrList function was too afraid or too lazy to go into old code and change all calls to ClearPtrList() to read ClearPtrList(FALSE). This fear is baseless and laziness unforgivable. It is fairly simple to drop the default and compile the old code as is. The compiler will do the work of spotting which places ClearPtrList() is called and complain about a missing parameter. These can then be changed to ClearPtrList(FALSE). Programmers writing new code will have to look at the declaration, exercise their brains and figure out what to do with the parameter and as a consequence with objects in their list. This will take programmer time but it is worth spending time thinking about it when calling the function initially than writing a whole load of code and realizing it needs to be debugged for a memory leak later!
I know some code maintainers will say I have been unfair and selected a typical example to my favour. What if the function was:
void GoodOldCode(); // This function does some arcane stuff
And the modification involves adding an optional bit of trailing code at the end. Well then instead of using a default parameter to execute the optional trailing code, I would suggest writing a new function which would call GoodOldCode() and do the new additional stuff at the end. If variables initialized by GoodOldCode are needed it would be a good idea to move common initialization stuff out of GoodOldCode() into a new function now. Maintenance involves improving code, not just changing it to do new things.
In conclusion, default function parameters are a convenience and not essential to programming in C++. You would have lived without them if the designers of C++ hadn’t opted to put them in. So forget they exist or use them with great caution. Parameters alter the behaviour of a function based on what the caller wants and when you supply a default parameter to a function, you take responsibility of thinking about where your function will be called from. In some cases you will have a fairly good idea, but in most cases you wont. As your function grows older and more popular, it will be called from places where you would have thought the caller would use their brains and supply the correct parameters. But unfortunately they won’t, they’ll simply rely on your default and decide their mental powers are better utilized in thinking about the immediate problem they are solving rather than in thinking about how an old function works.