To default, or not to default, that is the question…


One of the “interesting” features of C++ is the ability to default the value of parameters to a method.

It’s one of those features that I’m sure that Bjarne Stroustrup thought was just flat-out neat (there are a number of feature in C++ that fall into the broad category of “convenience features”, I’m pretty sure that defaulted parameters is one of them).

One of the developers (Nick) in my group was working on a fix for a bug, the fix required that he add a new parameter to a relatively common routine.

It turned out that for all of the calls to that routine, except for one, the new parameter would have the same value.

Nick’s suggestion was to add the new parameter as a defaulted parameter because that way he wouldn’t have to change that much code.

Eventually I vetoed the idea, because of the following problem.

 

Let’s say you have a function foo:

HRESULT Foo(DWORD param1, BOOL param2=FALSE)
{
    :
    :
}

Everything’s great – your code’s running, and it’s all great.

What happens six months later when you need to change the signature for Foo to:

HRESULT Foo(DWORD param1, void *newParam, BOOL param2=FALSE)
{
    :
    :
}

Well, in this case, you’re in luck, you can simply compile the code and the compiler will happily tell you every function that needs to change.

On the other hand, what if the change was:

HRESULT Foo(DWORD param1, DWORD newParam, BOOL param2=FALSE)
{
    :
    :
}

Now that’s a horse of a different color.  The problem is that the types for BOOL and DWORD are compatible.  It means that any code that specified a value for param2, like:

    hr = Foo(32, TRUE);

is still going to compile without error.  The compiler will simply interpret it as:

    hr = Foo(param1=32, newParam=1, param2=FALSE);

Now the language lawyer’s are going to shout up and down that this is a design problem in Windows, the BOOL and DWORD types shouldn’t have both been defined as “unsigned long”, that instead param2 should have been defined as “bool”.

The problem is that you STILL have problems.  If param2 was defined as ‘bool’, what happens if you need to add a non default parameter that’s of type ‘bool’?  You’re back where you were before.

Or you could have:

HRESULT Foo(DWORD param1, int newParam, short param2=3)
{
    :
    :
}

In this case, the automatic promotion rules will quite happily promote a short to an int without a warning.

There have been dozens of times when I’ve discovered bugs that were introduced by essentially this pattern – someone added a parameter to a function that has defaulted parameters, there was an automatic conversion between the defaulted parameter and the newly added parameter, and what was a simple change all of a sudden became a bug.

Comments (32)

  1. Anonymous says:

    Well…you can remove the ‘default’ing at the point in time when it DOES create a problem (ie when you are adding third parameter) rather than where it is unlikely to create a problem (ie when adding the second parameter). A quick fix – thoughtfully applied – need not be costly in the long run.

    My usual formula for adding a default parameter to an existing function is: Add it as non-default parameter. Look at ALL the compiler errors, fix those that would want to use the non-default value and then revert to default parameter.

  2. Anonymous says:

    Named default parameters sure are nice about this, aren’t they. 😉 To bad they’re such a hassle to write compilers for, they’re so useful once you get used to them.

  3. Anonymous says:

    If you combine default parameter values with inheritance, there are even more potential gotchas, for example:

    #include <stdio.h>

    struct B {

    virtual void Test(int i = 10) { printf("B::Test, i is %dn", i); }

    };

    struct D : public B {

    virtual void Test(int i = 20) { printf("D::Test, i is %dn", i); }

    };

    int main(int argc, char* argv[])

    {

    D d;

    B* pb = &d;

    pb->Test();

    return 0;

    }

  4. Dean Harding says:

    The other thing I don’t like about default parameters is that they’re actually applied at the caller, not the callee. So if you have:

    int foo(int a, int b = 10);

    But you decide later that 20 is a better default and change it to:

    int foo(int a, int b = 20);

    Then you have to make sure you recompile everything that calls foo with the default, otherwise it won’t pick up the new value.

    Though I guess in general, when you change a header in C++, you have to make sure you re-compiled everything that touches it anyway (even though "everything" may be in multiple DLLs or whatever).

  5. Anonymous says:

    I call BS

    You use the refactoring tool in VS 2005 to smartly handle this for you throughout the codebase

  6. Anonymous says:

    Geez, why is anyone defending these silly things?  There is NO REASON to use default parameters; working around them is easy, and doesn’t expose you to the sorts of problems Larry’s describing.

    If you start with function "Foo" with one parameter:

      HRESULT Foo(DWORD param1)

      {

         (implementation here)

      }

    …and you need to add that boolean, you just create a new function with two parameters, move the implementation there, and make the old one call the new one, passing the desired default value:

      HRESULT Foo(DWORD param1, BOOL param2)

      {

         (implementation moved here)

      }

      HRESULT Foo(DWORD param1)

      {

         return Foo(param1, FALSE);

      }

    You still get no errors, but now if you add a third parameter, in any order… kein Problem.  This is easy.  Why ask for trouble for such a minuscule "benefit".

  7. Barry, I’m not 100% sure that’ll work – if you add:

    HRESULT Foo(DWORD param1, DWORD newParam, BOOL param2)

    and change Foo(DWORD param1, BOOL param2) to:

    HRESULT Foo(DWORD param1, DWORD param2)

    You’re pretty much in the same spot.  The only way to make this work is to keep the Foo(DWORD, BOOL) the same, but that precludes your adding Foo(DWORD, DWORD).

    On the other hand, that may not really matter.

  8. Anonymous says:

    > Now the language lawyer’s are going to shout up and down

    > that this is a design problem in Windows, the BOOL and

    > DWORD types shouldn’t have both been defined as "unsigned

    > long",

    Wrong.  Designers (Enterprise Architects) would call that a design problem.  Language lawyers would say that the language permits exactly that.

    > What happens six months later when you need to change

    > the signature for Foo to:

    > HRESULT Foo(DWORD param1, void *newParam,

    >    BOOL param2=FALSE)

    Designers (Enterprise Architects) will be confused.  Why does that happen six months later, instead of the following:

    HRESULT Foo(DWORD param1, BOOL param2=FALSE, void *newParam=NULL)

    As a matter of administrative fiat, I don’t see any problem with either allowing or prohibiting the use of default parameters.  Enterprise Architects do not have to agree with each other about whether it’s a good thing to allow or not.

    As a coder, for unknown reasons I have a tendency in C++ to code parameters in calls even when they could be defaulted, but in VB to omit unneeded parameters and let them default.  If I see a certain style in existing code then I try to stick to it in any changes.

  9. Tim Smith says:

    The thing is, you can’t just add the arguments in any order.  

    I personally have nearly outlawed the default parameters in my group for this very reason.  We use the forwarding techinque Barry talks about, but you can’t just add the arguments anywhere.  Like Larray mentioned, you can still have problems.

    In Larry’s example, the only reason we didn’t add the new argument to the end is because of the default argument.  Once you get rid of default arguments, then there is little reason to add in the middle.

    There are some other problems to the forwarding method, but in general, I have had less problems with forwarding than with default arguments.

  10. Dean Harding says:

    > Designers (Enterprise Architects) would call that a design problem.

    Why would "Designers (Enterprise Architects)" be worried about implementation details such as what data types you use?

    > Designers (Enterprise Architects) will be confused.  Why does that happen six

    > months later, instead of the following:

    > HRESULT Foo(DWORD param1,

    >    BOOL param2=FALSE, void *newParam=NULL)

    Perhaps newParam doesn’t HAVE a "default" value.

  11. sarathc says:

    What about "STRICT" type checking in windows? are you supporting this? in any of my projects, I didnt use that 🙁

  12. Anonymous says:

    Stroustrup described default arguments in D&E as "logically redundant and at best a minor notational convenience" but explained that they were introduced to C with Classes before general function overloading. At that point, default arguments were trying to solve a different problem; they weren’t simply added because Bjarne Stroustrup thought they were ‘neat’… unless of course you consider overloading and compile-time polymorphism to be nothing more than neat convenience features.

    As you point out, default arguments shouldn’t be used as a bandage to save a lazy developer some work. Like any other language feature, it needs to be understood and used judiciously.

    As a closing thought, BS wrote in one of his books that he expected the average number of arguments to a function to sink to below two as people learnt to design better abstractions. Would you say that he is wrong, or has the transition away from C-like thinking within Microsoft been too slow?

  13. Anonymous says:

    You only have to look at the .NET Framework to realize that default arguments are occasionally useful; it’s silly to expose 30 overloads and have to manually code 27 of them as forwarders when you could have only one show up in the IDE’s drop-down whose default arguments show you exactly what you are going to get by omitting arguments.

    IMO, I don’t believe this is a case of default argument vs. overloading — it’s a case of where the new function really should have used a different name.

  14. Anonymous says:

    Thursday, July 20, 2006 10:45 PM by Dean Harding

    [Larry Osterman:]

    >>> Now the language lawyer’s are going to shout up and down

    >>> that this is a design problem in Windows, the BOOL and

    >>> DWORD types shouldn’t have both been defined as

    >>> "unsigned long",

    >>

    [Norman Diamond:]

    >> Designers (Enterprise Architects) would call that a design

    >> problem.

    >

    > Why would "Designers (Enterprise Architects)" be worried

    > about implementation details such as what data types you

    > use?

    OK.  SOME designers would worry about the effects of designing data types that are linguistically interchangeable when the data’s meanings are not interchangeable, because the design makes some kinds of errors compileable instead of helping catch some kinds of errors at compile time.  Exactly as you imply SOME designers would not care.

    Nonetheless the data type design which might be a misdesign is not language lawyer fodder.  The language unambiguously defines what is going to happen in these cases.

    >> Designers (Enterprise Architects) will be confused.  Why

    >> does that happen six months later, instead of the following:

    >> HRESULT Foo(DWORD param1,

    >>    BOOL param2=FALSE, void *newParam=NULL)

    >

    > Perhaps newParam doesn’t HAVE a "default" value.

    That makes no sense.  The current version of the function has no newParam.  Current callers get current features of the function without specifying any value for newParam.  If the future function will be an extension of the current one rather than an incompatible completely new function, then current callers will still get the current features where newParam’s value will not play any role.  New callers that will want new features will specify some non-default value in order to get them.

  15. Anonymous says:

    There’s always the FooEx option (not to be confused with FedEx) which has the advantage (?) of being non C++ compatible if you’re in to that sort of thing. Depending on the context this can be a good thing (eg Win32 API) or a bad thing (eg Win32 API).

    // original version

    HRESULT Foo(DWORD param1)

    {

       return FooEx(param1, FALSE);

    }

    // steroid version

    HRESULT FooEx(DWORD param1, BOOL param2)

    {

       // implementation here.

    }

  16. sarathc, STRICT just makes all the various USER and GDI objects have unique handle types.  It dosn’t change the type of BOOL vs DWORD (as far as I know).

  17. Anonymous says:

    I agree with steveg and prefer the FooEx option.  

    It has the added benefit that if the function is exposed from a dll none of the clients will require a recompile.

    I also tend to avoid the default parameters methodology for the reasons Larry describes.  

  18. Anonymous says:

    Larry:  First you have

      HRESULT Foo(DWORD param1)

    Then you add

      HRESULT Foo(DWORD param1, BOOL param2)

    Now if you add

      HRESULT Foo(DWORD param1, DWORD newParam,

               BOOL param2)

    *and* you also need to add

      HRESULT Foo(DWORD param1, DWORD newParam)

    then you’ll get a compile-time error because the second and fourth cause an ambiguity when someone tries to call them.

    At that point, you need a more extensive correction, but the point is that you KNOW it.  It doesn’t just sneak up on you.

  19. nksingh says:

    Depending on the frequency of the function, this seems like a problem that could easily just be solved by just listening to the compiler errors.  If there are tons of calls, then a simple regular expression would do the trick.  I think Larry’s totally right.

  20. Anonymous says:

    This is not just a problem with default argument values.  It is an instance of a classic (and ancient) problem whose solution I learned long ago from my friend Bill:  If you change HOW it works, then change the NAME, too.  This is named above the FooEx solution.  Sure, C prototypes are great and help a lot.  And function overloading is a pay-less-now-vs.-more-later trap.  Call me a fossil, but I’ll take a solution that works in asm as well as C++.  So there.

  21. Anonymous says:

    Oops. I meant "If you change the INTERFACE (not "how it works"), then change the NAME, too."  

  22. Anonymous says:

    A better analysis is that adding default parameters is a short-term strategy, not a long-term one.  That is, the first time you have to change the method, adding a default may work.  It might even work the second time.  Eventually, though, the list of defaults gets weird and ungainly.

    Since Microsoft should be designing interfaces for the long run, it should not use defaulted parameters.

  23. Defaultable parameters are a WONDERFUL idea.

    They’re just not especially compatible with conventional ordered-parameter-lists.

    For languages that support named parameters[1], you get all the benefits of defaultable parameters, without this subtle bug haven.

    [1] e.g., Visual Basic for Applications; TRANSACT-SQL; and to a lesser extent, Perl

  24. Anonymous says:

    I’ve had similar problems when using default arguments in a scripting environment.  

    For example, one of the Microsoft security patches changed one of the default arguments (that we didn’t specify of course), and suddenly our scripts would no longer run on computers with the latest patches.  

    Of course this is to be expected with a scripting language, but who would have ever thought that they might change the "default" value in a future release!  

  25. Anonymous says:

    This risk with default parameters can’t happen in my team…

    It used to be that people would create function like Func( …, FALSE, TRUE, FALSE )  but now I have banned that.

    Now, I force developers to create a descriptive enum for these FALSE/TRUE magic values.  This makes sense for various reasons :

    1) For code readability. I’ve never seen a function where it was acceptable to have more than one bool value.  You can’t guess that the other values are for without reading the function documentation

    2) the parameter is now of a specific type, that enum type, so the compiler will block passing it just any value without having had to think about what you really wanted to pass

    We still use default values.  When you decide to put a non-default, you really have to think about it.

    If we add a parameter at the wrong place, then the compiler will not let it pass as this blog entry describes.

    If people are wondering, isn’t that poluting the global namespace with tons of enums? Well, no, most of our code is COM interface, and the enums are defined inside the interface.  If they’re helper functions, they’re in a namespace/

    An ficticious example of function would be something like

     CheckPathName( szPath, IFicticious::DoNotAcceptUNC );

    which is a whole lot better than

     CheckPathName( szPath, FALSE );

  26. Anonymous says:

    Saturday, July 22, 2006 2:56 PM by Maurits

    > Defaultable parameters are a WONDERFUL idea.

    > For languages that support named parameters[1],

    [1]  JCL.  Nonetheless everyone hated it.  IBM’s macro assembler had a syntax pretty close to that, but for some reason everyone didn’t hate it.

    Know any examples more than 42 years old?

    Monday, July 24, 2006 9:16 PM by Ulric

    > Now, I force developers to create a descriptive enum for

    > these FALSE/TRUE magic values.

    You mean FALSE doesn’t descriptively say which direction the data are being transfered between your program’s variables and the controls in the screen?  It’s exactly the opposite direction of TRUE, you know?  And TRUE is perfectly obvious because it’s the opposite direction of FALSE.

    Now all we have to do is figure out which direction should be the default ^_^

  27. Anonymous says:

    I think C++ should have allowed parameters with defaults to appear before parameters without defaults.

    HRESULT Foo(DWORD param1, BOOL param2=FALSE, DWORD newParam);

    hr = Foo(32, , 42);

    Then just remember to put new parameters at the end, not in the middle.

    Doesn’t VB have this feature?

  28. mlacey says:

    I once discovered the hard way that it’s easy to make cut & paste errors when using functions with default parameters. This issue applies equally well to overloaded functions.

    For example, I once had to update some code so that calls to a particular function were done conditionally, e.g. replacing:

       func(pSomeClass, pSomeSymbol);

    with

       if (pSomeClass) {

           func(pSomeClass, pSomeSymbol);

       } else {

           // do some other stuff

       }

    What I failed to notice was that one of those calls was:

       func(pSomeClass, pSomeSymbol, o);

    and I replaced it with:

       if (pSomeClass) {

           func(pSomeClass, pSomeSymbol);

       } else {

           // do some other stuff

       }

    This was missed in a code review, and passed our check-in testing.

  29. Anonymous says:

    > What happens six months later when you need to change

    > the signature for Foo to:

    > HRESULT Foo(DWORD param1, void *newParam,

    >    BOOL param2=FALSE)

    > Well, in this case, you’re in luck

    That’s also a problem (it bit me quite recently). Imagine a call:

    Foo(32,FALSE);

    FALSE is defined as 0, which is perfectly valid value for void *. So you get:

    Foo(32,NULL,FALSE);

  30. Anonymous says:

    There’s a C++0x proposal to make "default" a valid argument for function calls. The result is that you can add additional parameters after defaulted arguments.

    In general I disagree with the "default arguments" bashing. The effect is equivalent to overloading. And the solution is easy: when you add a parameter, you look at the full call graph first. No full call graph (you might ship a DLL)? Then you don’t add parameters where it causes problems; you add FooEx.

    Talking about that, I think windows.h should include a WinCpp.h which provides defaulted Foo overloads for all the FooEx functions (The default-argument problem won’t matter, because the original C Foo function won’t have defaults)

  31. Anonymous says:

    > There’s a C++0x proposal to make "default" a valid argument for function calls. The result is that you can add additional parameters after defaulted arguments.

    If you’re going to have to type "default", you might as well make it a non-default parameter and type in its actual value!