Why can’t I pass a reference to a derived class to a function that takes a reference to a base class by reference?


"Why can't I pass a reference to a derived class to a function that takes a reference to a base class by reference?" That's a confusing question, but it's phrased that way because the simpler phrasing is wrong!

Ths misleading simplified phrasing of the question is "Why can't I pass a reference to a derived class to a function that takes a base class by reference?" And in fact the answer is "You can!"

class Base { }
class Derived : Base { }

class Program {
  static void f(Base b) { }

  public static void Main()
  {
      Derived d = new Derived();
      f(d);
  }
}

Our call to f passes a reference to the derived class to a function that takes a reference to the base class. This is perfectly fine.

When people ask this question, they are typically wondering about passing a reference to the base class by reference. There is a double indirection here. You are passing a reference to a variable, and the variable is a reference to the base class. And it is this double reference that causes the problem.

class Base { }
class Derived : Base { }

class Program {
  static void f(ref Base b) { }

  public static void Main()
  {
      Derived d = new Derived();
      f(ref d); // error
  }
}

Adding the ref keyword to the parameter results in a compiler error:

error CS1503: Argument '1': cannot convert from 'ref Derived' to 'ref Base'

The reason this is disallowed is that it would allow you to violate the type system. Consider:

  static void f(ref Base b) { b = new Base(); }

Now things get interesting. Your call to f(ref d) passes a reference to a Derived by reference. When the f function modifies its formal parameter b, it's actually modifying your variable d. What's worse, it's putting a Base in it! When f returns, your variable d, which is declared as being a reference to a Derived is actually a reference to the base class Base.

At this point everything falls apart. Your program calls some method like d.OnlyInDerived(), and the CLR ends up executing a method on an object that doesn't even support that method.

You actually knew this; you just didn't know it. Let's start from the easier cases and work up. First, passing a reference into a function:

void f(SomeClass s);

...
   T t = new T();
   f(t);

The function f expects to receive a reference to a SomeClass, but you're passing a reference to a T. When is this legal?

"Duh. T must be SomeClass or a class derived from SomeClass."

What's good for the goose is good for the gander. When you pass a parameter as ref, it not only goes into the method, but it also comes out. (Not strictly true but close enough.) You can think of it as a bidirectional parameter to the function call. Therefore, the rule "If a function expects a reference to a class, you must provide a reference to that class or a derived class" applies in both directions. When the parameter goes in, you must provide a reference to that class or a derived class. And when the parameter comes out, it also must be a reference to that class or a derived class (because the function is "passing the parameter" back to you, the caller).

But the only time that S can be T or a subclass, while simultaneously having T be S or a subclass is when S and T are the same thing. This is just the law of antisymmetry for partially-ordered sets: "if a ≤ b and b ≤ a, then a = b."

Comments (23)
  1. Uh… what happend to "not a .NET blog?"

  2. Random832 says:

    VB has a more clear error message:

    Option Strict On disallows narrowing from type ‘System.Windows.Forms.Control’ to type ‘System.Windows.Forms.Panel’ in copying the value of ‘ByRef’ parameter ‘x’ back to the matching argument.

    Well, more clear in some ways. It could be incorrectly taken to suggest that Option Strict Off is an acceptable solution, but it at least explains about "copying the parameter back to the argument" [which isn’t [i]exactly[/i] what happens, but it serves as a valid explanation of why this is a problem]

  3. acq says:

    To Maurits: Relax it’s a traditional CLR week.

    I don’t program in .NET but I highly recommend reading Raymond’s CLR week posts. I even claim that from older CLR week posts most of the readers could actually learn something that they can use in their C/C++ programming.

  4. Erzengel says:

    Maurits: It’s .NET week. It said so last post I believe. It happens every year.

    Raymond: The runtime could throw an exception instead. For me, when I ran into this, it was because a reference to a struct got changed to a reference to a class, and so it became a double reference. The function was only changing the members of the variable, not assigning to the variable itself, so all that was necessary was to remove the ref. In that case there was no "going around typing", but it also was unnecessary to use the ref.

    After all, I can "(Derived)Base" to get around typing just as much as a ref would, but I’ll get a runtime exception if Base doesn’t actually contain a Derived.

  5. Tony Cox [MSFT] says:

    @Erzengel: Yes, the runtime could check all the types and throw an exception, but there would be a performance cost to that.

    Proper support for co/contra-variance is a better solution, and in fact is the route being taken.

    (As an aside, CLR arrays are co-variant. Which is generally regarded as somewhat unfortunate these days.)

  6. Anonymous says:

    This is the same thing as converting Derived** to Base** in C++; see http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.2 for the C++ explanation.

  7. Erzengel says:

    Tony: Yes it does have a cost, but I was under the impression we were already paying that cost… It happens with arrays, after all.

    I’m not saying that’s what I’d want, really. Compile time protection against that is a good thing. It’s just an intellectual exercise that, since it does it differently elsewhere, what if you applied that here?

  8. wcoenen says:

    I like that the compiler does these sort of checks, but I don’t understand why it doesn’t stop me from shooting myself in the foot for a similar case with arrays:

    // compiles but blows up at runtime

    object[] test = new string[] {“foo”, “bar”};

    test[0] = new object();

    [But who actually writes code like that? You would much more likely write
    string[] test = new string[] {“foo”, “bar”};
    test[0] = new object(); // compiler raises error here

    It seems odd to create special “constants propagation” code for something nobody actually writes. -Raymond
    ]
  9. pete.d says:

    <i>Ths misleading simplified phrasing of the question is "Why can’t I pass a reference to a derived class to a function that takes a base class by reference?" And in fact the answer is "You can!"<i>

    I believe you’ve mis-worded the misleading simplified phrasing of the question.  It should read "Why can’t I pass a reference to a derived class to a function that takes a reference to a base class [by value]?"

    The terms "by reference" and "by value" have very specific meanings, in C# and elsewhere.  In the "You can!" code example, the "Base b" argument to the method "f" is a "by value" argument, and when "f" is called, the variable "d" is passed "by value" to "f".  The value is a reference, but it’s passed "by value", not "by reference".

  10. Erzengel says:

    [But who actually writes code like that? You would much more likely write

    string[] test = new string[] {“foo”, “bar”}; …-Raymond]

    Well, consider this possibility:

    void MyFunction(object[] Params)

    {

       switch(this.MyEnum)

       {

           case Enum.MyObject:

               Params[0] = new MyObject();

               break;

           case Enum.OtherObject:

               Params[0] = new OtherObject();

               break;

           default:

               throw new Exception();

    }

    void UseMyFunction()

    {

       OtherObject[] Output = new OtherObject[5];

       MyFunction(Output);

    }

    If this.MyEnum is Enum.MyObject, you’ve got a runtime error.

    This is very, very bad code, but it is also valid code. I would expect something LIKE this could possibly happen. Especially if the programmer, when writing this code, says “Well, any function that calls UseMyFunction would set this.MyEnum to Enum.OtherObject, so it’s OK to create the array as an OtherObject array.” Then, lo, a few weeks later, someone makes a change and that’s no longer the case.

    [Well that’s not the same as the original code, which is all within a single function. This error requires cross-function data flow analysis. Even so, it raises a compiler error in MyFunction based on whether or not the function UseMyFunction exists. Most of the time, people don’t expect this sort of “spooky compiler action at a distance.” -Raymond]

    It’s always an annoyance to me when someone focuses on the example code. Programming is extremely complicated. There are massive layers of indirection and generalization that make it difficult to know what’s happening and when. It’s usually because of these layers that esoteric things go wonky. Focus on the actual problem, not the steps to reproduce.

    Speaking as someone whose worked in Quality Assurance, I have seen times where bugs have been dismissed because “Well that seems extremely unlikely that someone would do that”, but they don’t realize that that was an example, that was the easiest, and clearest, way to reproduce it. But the bug happens everywhere, it’s just not so easy to show it everywhere. Focus on the problem, not the extremely simplified steps.

  11. CLR inherited covariant array subtyping from Java — there must have been quite a strong marketing pressure for the language core not to obviously miss (mis)features Java had. I think it speaks well of C# that it does not extend the same wartiness to other primitives of its own invention.

    The covariance of Java arrays may originally have been an oversight in the type system, but had a number of (more or less) legitimate uses before generics were added to the language. For example, it allows a general container class to have a

      void writeAllElementsIntoArray(Object[] dest)

    method, and let the caller supply an array of the type it knew the elements to have.

  12. hexatron says:

    Stuff like this makes me glad i never figured out all that object-oriented stuff (he said while lost in a deep/shallow copy bug in arch-competitor’s IPhone picture-taking doohickey.)

  13. pete.d says:

    "The covariance of Java arrays may originally have been an oversight in the type system, but had a number of (more or less) legitimate uses before generics were added to the language."

    Since Java generics aren’t reified, as in C#, I think there is still a legitimate use for covariant arrays in Java.  For that matter, the lack of generics in C# 1.0 may be part of the justification for covariant arrays in the CLR and C#.

  14. JM says:

    @Erzengel: I would simply repeat "who actually writes code like that", but with a dash of "if you do write code like that and you’ve determined that you need it, the compiler is no longer able to help you, deal with it".

    The problem here is array covariance, which as this example demonstrates is a double-edged sword. Understanding it and when it causes problems is the programmer’s burden. The problem is in any case not with the compiler — static type checking only goes so far, that’s nothing new. The compiler isn’t there to catch all errors in code that’s "very bad" but "valid".

    It’s also good to keep in mind that all the complexity in "programming is extremely complicated" is put there by the programmers. It might be necessary complexity, but it’s all of our own making.

  15. C++ programmer says:

    "Our call to f passes a reference to the derived class to a function that takes a reference to the base class. This is perfectly fine."

    Is it? I do not know much about C# currently but I would have thought this would slice the class as it would in C++. Is that not the case?

  16. Neil says:

    Of course sometimes you want to do the reverse but you can’t do that either except by collecting everything up in the return value.

  17. Vincent says:

    Just a short note. I understand that law of antisymmetry as the squeeze theorem in calculus.

  18. Mike Dimmick says:

    C++ programmer: there is no equivalent of C++ objects on the stack in C#. When you declare a name of class type within a function body, you are not actually declaring an object. You’re declaring a *reference* to an object. However, if the variable is a ‘value type’ – either a fixed-size built-in type such as int, float, double, bool, or a ‘struct’ – then it actually *is* an object on the stack.

    So, to review: if A is a ‘class’, in C#:

    A a;

    is the equivalent of

    A* a;

    in C++.

    That means when you pass a reference type (class type) to a method, you are *automatically* passing by reference. Because class types are never passed by value – and only class types support inheritance – slicing is not possible.

  19. manicmarc says:

    Interesting – I program c# every day and I’ve never had a need to do this, so never noticed it. I avoid using ref where possible.

  20. Random832 says:

    Yeah, I messed up doing this from memory – all "const"s not before a T should be moved one position to the right in the left column

  21. mauke says:

    @hexatron: "Stuff like this makes me glad i never figured out all that object-oriented stuff" — well, you’ve got the same problem in plain C. If you have a type T, ‘T *’ is convertible to ‘const T *’; but ‘T **’ can’t be converted to ‘const T **’, for exactly the same reason: you could use it to make a ‘T *’ point to a ‘const T’.

  22. Random832 says:

    @mauke: Of course, “T **” can be converted to “T const **”. It’s a matter of asking the right question.

    typedef (whatever) T;

    typedef T *PT;

    typedef const T *PCT;

    T * = PT = pointer to T

    const T * = PCT = pointer to const T

    T const * = const PT = const pointer to T

    [This is where your logic falls apart. “T const *” is a pointer to const T. You’re confusing it with “T * const” which is a const pointer to T. -Raymond]

    T ** = PT * = pointer to pointer to T

    T const ** = const PT * = pointer to const pointer to T

    const T ** = PCT * = pointer to pointer to const T

    T * const * = PT const * = const pointer to pointer to T

    “pointer to {FOO}” is convertible to “pointer to const {FOO}”

    “const {FOO}” is an actual const, which cannot be assigned to, but can be assigned to a {FOO}.

  23. Florian says:

    I don’t get the point. T** can be converted to T*const* but not to const T** (as mauke said).

Comments are closed.

Skip to main content