To base() or not to base(), that is the question

Today I’ve been reviewing the ECMA-334 C# specification, and in particular the section about class instance constructors.

I was struck by this piece in a clause about default constructors:

If a class contains no instance constructor declarations, a default instance constructor is automatically provided. That default constructor simply invokes the parameterless constructor of the direct base class.

I believe this to be incorrect, and indeed it is, as shown here (in C# 6 code for brevity, despite this being the C# 5 spec that I’m reviewing; that’s irrelevant in this case):

using System;

class Base
{
    public int Foo { get; }

    public Base(int foo = 5)
    {
        Foo = foo;
    }
}

class Derived : Base
{    
}

class Test
{
    static void Main()
    {
        var d = new Derived();
        Console.WriteLine(d.Foo); // Prints 5
    }    
}

Here the default constructor in Derived clearly doesn’t execute a parameterless constructor in Base because there is no parameterless constructor in Base. Instead, it executes the parameterized constructor, providing the default argument value.

So, I considered whether we could reword the standard to something like:

If a class contains no instance constructor declarations, a default instance constructor is automatically provided. That default constructor simply invokes a constructor of the direct base class as if the default constructor contained a constructor initializer of base().

But is that always the case? It turns out it’s not – at least not in Roslyn. There are more interesting optional parameters we can use than just int foo = 5. Let’s have a look:

using System;
using System.Runtime.CompilerServices;

class Base
{
    public string Origin { get; }

    public Base([CallerMemberName] string name = "Unspecified",
                [CallerFilePath] string source = "Unspecified",                
                [CallerLineNumber] int line = -1)
    {
        Origin = $"{name} - {source}:{line}";
    }
}

class Derived1 : Base {}
class Derived2 : Base
{
    public Derived2() {}
}
class Derived3 : Base
{
    public Derived3() : base() {}
}

class Test
{
    static void Main()
    {
        Console.WriteLine(new Derived1().Origin);
        Console.WriteLine(new Derived2().Origin);
        Console.WriteLine(new Derived3().Origin);
    }    
}

The result is:

Unspecified - Unspecified:-1
Unspecified - Unspecified:-1
.ctor - c:\Users\Jon\Test\Test.cs:23

When base() is explicitly specified, that source location is treated as the “caller” for caller member info attributes. When it’s implicit (including when there’s a default constructor), no source location is made available to the Base constructor.

This is somewhat compiler-specific – and I can imagine different results where the default constructor could specify a name but not source file or line number, and the declared constructor with an implicit call could specify the name and source file but no line number.

I would never suggest using this little tidbit of Roslyn implementation trivia, but it’s fun nonetheless…

30 thoughts on “To base() or not to base(), that is the question”

  1. Jon – very interesting bit of Trivia. One minor typo: in the first code sample you have: “Foo = 5;” for the assignment that should be “Foo = foo;”. Of course, with that typo corrected, the behavior is still as described in your article.

    Liked by 1 person

  2. Is it the C# spec that is incorrect, or the C# implementation that is incorrect? By definition, the spec says how the language should work. If the language doesn’t do what the spec says, isn’t it an implementation problem?

    Like

    1. “Is it the C# spec that is incorrect, or the C# implementation that is incorrect? By definition, the spec says how the language should work. If the language doesn’t do what the spec says, isn’t it an implementation problem?”

      So if a spec had wording that implied that, say, every arithmetic operation must throw an exception, then the spec isn’t incorrect?

      In this case, the spec says that the default constructor of the derived class “simply invokes the parameterless constructor of the direct base class” but, as Jon noted (did you even read it?) , there is no such constructor in the base class. How is the implementation incorrect when the spec is incoherent?

      Like

  3. This is somewhat compiler-specific

    For comparison, in mcs the implicitly provided constructors behave identical to the explicitly provided constructor:

    $ mono skeet.exe 
    .ctor - /Users/jon/tmp/csharp/skeet.cs:16
    .ctor - /Users/jon/tmp/csharp/skeet.cs:19
    .ctor - /Users/jon/tmp/csharp/skeet.cs:23
    

    Liked by 1 person

  4. Hm, wouldn’t it be a good idea to specify all this behavior instead of leaving it unspecified? Better to make a decision and force all compilers to the same rules.

    Like

  5. The specification says “If a class contains no instance constructor declarations…”, but you HAVE defined an instance constructor. If the specification said “If a class contains no parameterless instance constructor declaration” then it would be wrong.

    Like

    1. Sorry, I read that wrong. I see it now, but it still seems “odd”. Your base declares an instance constructor with an optional parameter. Are you sure the problem isn’t with how instance constructors with an optional parameter is implemented? It wouldn’t make any sense to have both an single optional parameter AND a parameterless constructor at the same time.

      Like

      1. I don’t think there’s any problem in the implementation – I think it’s just a matter of the spec needing to be updated in the light of optional parameters.

        Like

  6. I’m surprised this works. Won’t there be nasty problems if the default value of the base constructor parameter is changed and it resides in a different assembly (meaning the assembly with the derived class is not re-compiled and simply uses the newer base class version)?
    While I believe that this is a problem for any use of default parameters, it appears to be even more severe here as the base class ctor call is virtually invisible.

    Like

    1. No more than if there were an explicitly-specified constructor, with base() either being implicit or explicit. Yes, it’s a concern – but far from unique to this situation. Basically, changing the default argument of anything is a breaking change.

      Like

  7. As per the wording of the spec as Base provides a constructor, a default constructor will not be created for it, subclasses would need to call the constructor provided i.e. Base(int foo = 5). As such, I don’t think it’s incongruous for Derived to implicitly call Base(), would you have the compiler issue an error and force the developer to explicitly declare a constructor for the Derived class?

    As Jonathan Pryor pointed out the behavior in the 2nd example seems to differ between implementations of Rosalyn. I think the 2nd example is illustrating a bug in the Microsoft .NET implementation of Rosalyn.

    Like

    1. would you have the compiler issue an error and force the developer to explicitly declare a constructor for the Derived class?

      No – but that’s what the spec says at the moment. That’s why I’m working to change the spec.

      I think the 2nd example is illustrating a bug in the Microsoft .NET implementation of Rosalyn.

      I think it would be reasonable for it to always provide the member name. For the filename and line number… that’s harder to say, IMO. Where there’s an explicitly declared constructor, I think it makes sense to specify the filename, as we know where the member is declared – but including a line number suggests that there is a constructor initializer in the file (on that line), when there might not be. I think it would be sane to have:

      • Default constructor? Name, no filename, no line number
      • Explicitly-declared constructor with implicit base() call? Name, filename, no line number
      • Explicitly-declared constructor with explicit base() call? Name, filename and line number

      Like

  8. ECMA-334(4th) uses the term “parameterless” 19 times. I bet there are fun bugs or, at the very least, unexpected behavior lurking behind a lot of them when combined with optional arguments. Perhaps the solution is to define “parameterless” as part of overload resolution, i.e. “can resolve an overload with zero actual arguments”?

    In fact, optional arguments probably show up in the new spec as part of overload resolution anyway.

    Like

Leave a reply to jonskeet Cancel reply