Optional argument corner cases, part four

Optional argument corner cases, part four

Rate This
  • Comments 18

(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


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.)

  • It looks like the covenience has its price.

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

  • 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?


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

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

  • @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).

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

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

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

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

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

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


  • @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 }.

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

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

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

Page 1 of 2 (18 items) 12