Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

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

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

  • Comments 32
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.

  • 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.
  • 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.
  • 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 %d\n", i); }
    };
    struct D : public B {
    virtual void Test(int i = 20) { printf("D::Test, i is %d\n", i); }
    };

    int main(int argc, char* argv[])
    {
    D d;
    B* pb = &d;
    pb->Test();
    return 0;
    }
  • 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).
  • I call BS

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

  • 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".
  • 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.

  • > 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.
  • 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.
  • > 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.
  • What about "STRICT" type checking in windows? are you supporting this? in any of my projects, I didnt use that :(
  • 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?
  • 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.
  • 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.
  • 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.
    }
Page 1 of 3 (32 items) 123