Optional argument corner cases, part four


(This is the fourth and final part of a series on the corner cases of optional arguments in C# 4; part three is here.)

Last time we discussed how some people think that an optional argument generates a bunch of overloads that call each other. People also sometimes incorrectly think that

void M(string format, bool b = false)
{
  Console.WriteLine(format, b);
}

is actually a syntactic sugar for something morally like:

void M(string format, bool? b)
{
  bool realB = b ?? false;
  Console.WriteLine(format, realB);
}

and then the call site

M(“{0}”);

is rewritten as

M(“{0}”, null};

That is, they believe that the default value is somehow “baked in” to the callee.

In fact, the default value is baked in to the caller; the code on the callee side is untouched and the caller becomes

M(“{0}”, false);

A consequence of this fact is that if you change the default value of a library method without recompiling the callers of that library, the callers don’t change their behaviour just because the default changed. If you ship a new version of method “M” that changes the default to “true” it doesn’t matter to those callers. Until a caller of M with one argument is recompiled it will always pass “false”.

That could be a good thing. Changing a default from “false” to “true” is a breaking change, and one could argue that existing callers *should* be insulated from that breaking change.

This is a fairly serious versioning issue, and one of the main reasons why we pushed back for so long on adding default arguments to C#. The lesson here is to think carefully about the scenario with the long term in mind. If you suspect that you will be changing a default value and you want the callers to pick up the change without recompilation, don’t use a default value in the argument list; make two overloads, where the one with fewer parameters calls the other.

(This is the fourth and final part of a series on the corner cases of optional arguments in C# 4; part three is here.)

Comments (18)

  1. never thought about so many corner cases says:

    It looks like the covenience has its price.

    I'm wondering why I haven't heard of these corner cases in C++ before…

  2. Allan Ferreira says:

    Eric,

    I'm not sure if this is a corner case, but, if the default values are baked in to the caller, why default declarations such as

    public void SomeMethod(ISomeInterface thing = new SomeClass())

    are not allowed? Why default values have to be a constant?

    Thanks

  3. Ben Voigt [Visual C++ MVP] says:

    @never: Probably because in the C++ compile model, you usually end up recompiling the caller.  Most default arguments are probably used on inlined functions anyway, which rather blurs the distinction between whether the default got baked in to the caller or callee, because the whole callee got baked in.

  4. sukru says:

    Also for most people, who are not writing compilers, the defaults usually make sense, and they rarely even need to know the specifics.

    How many times do you rebuild your libraries, but not executable, in your visual studio solution? By default it re-compiles every dependant, and you never run into this.

  5. CarlD says:

    @never  To further Ben Voight's comments, nearly all of these corner cases apply equally to C++, the interface-related ones being the exception (due to the fact that C++ has no formal concept of interfaces).

  6. Sylvain Defresne says:

    @Allan I think this is because the value is baked in the caller. If you allowed a non constant value, then a new value need to be created by each client before calling the function, and that could violate the principle of least surprise.

  7. fastcat@gmail.com says:

    @Allan: because the default value needs to be baked into an attribute on the callee method declaration, so that the compiler can see it when the caller builds.  Thus all of the constant-ness requirements that apply to custom attributes also apply to default values.

  8. Richard says:

    @Allan, Sylvain: This could also be a security hole. It's not so much of an issue with the simplified CAS in .NET 4, but if the callee doesn't have the same permissions as the caller, strange things might happen.

  9. Shuggy says:

    Eric,

    Any reason (other than confusion and perhaps edge case regressions for people using reflection) you didn't instead have the compiler transform thusly:

    default values are backed into the types on which they are defined as const values with synthetic names. Visibility is same as the underlying method's. The parameter is attributed with the 'ref' to the relevant variable (if this was a pain due to il meta data restrictions you could use a naming convention, though that is pretty evil I admit).

    Hide these defaults from the intellisense system entirely.

    When dependent code is compiled it does not embed the constant, it embeds the Type.NamedConstant indirection.

    So long as the name construction is solid (based on the method names plus the overload signature) then elimination of the default is a binary breaking change ("default value 'foo' has been removed"). as I believe it should be (you might disagree which would be good to know). If someone didn't want this they should supply proper overloads as before.

    If you wanted to tolerate removal (as opposed to changing) of the defaults as a non breaking change (using whatever was present at compile time) you could still allow that by some lazy semi reflective hackery that looked for the defined value and used the compile time constant if it was no longer present.

    Plenty of issues I'm sure, I just wondered if you considered something like this instead.

  10. Szindbad says:

    The same versioning issue apperas as in case of constants, I guess.

  11. Rob Manderson says:

    I can't even begin to imagine the thought processes of someone who thinks the compiler rewrites the callee. Of course,it might be that I lack imagination, but I've always thought the compiler would simply push the defaults as required when it compiled the caller code.

    This makes it pretty obvious that if you use a default, change the default but don't recompile calling code then you'll be passing the default values as they were at the time the caller was compiled.

    *shrug*

  12. Jamie. says:

    @Rob: IIRC, F#'s version of optional parameters changes to something like Eric's second version. Though in that case you explicitly write the "x = obj ?? new SomeObj()" code. But it doesn't feel like a huge stretch to just go from that to a model where (int x = 4) is transformed to (int?x) { if (x==null)x=4 }.

  13. Random832 says:

    "default values are backed into the types on which they are defined as const values with synthetic names. "

    Er, you do know what happens to const values if you change them and don't recompile, right?

  14. Shuggy says:

    Ooops yes that was a thinko, I meant to say static readonly…

  15. Shuggy says:

    Though actually thinking about it I like the translation into Option<T> like behaviour, so long as Option was done as a struct instead.

  16. Mark Seward says:

    Two thoughts:

    1.  Is the caller-rewrite behavior the same as VB.Net's behavior?  IOW, if I version a VB.Net class then call it from C# or vice versa, do I get consistent behavior?

    2. @Rob Manderson: With all respect, I think you didn't think quite enough.  At compile time for the callEE, it could easily have been transformed into the nested overloads model.  And then when the callER is compiled the appropriate generated callEE overload is selected.  And then also at run time when the compile-time type / run-time type issue is resolved.  As Eric pointed out in an earlier post, there are challenges with this approach.  But there's nothing which makes it logically impossible, or which makes "bake the callEEs default into the callER" inevitable.  Eric's decision to write 4 posts about this is IMO evidence the whole situation  is non-obvious.

  17. @Shuggy:

    The problem with your scheme is that it means that programmer loses control over the public surface of the class. Now any language targeting CLR that does not implement that default-arg scheme (which would be all existing ones) suddenly sees a lot of weird public static members in other classes.

    Adding generated private members is fair game for the compiler. Public ones, not so much.

  18. @Mark:

    Yes, the behavior is the same in VB. The only catch is that C# permits some changes to default values which are breaking to VB (on source level, of course, not binary). Specifically, in VB, if the base class changes the default argument for one of its virtual methods, then any VB overrides will stop compiling, whereas in C# they will compile just fine.

Skip to main content