An interesting polymorphism behavior/mis-behavior?

I’m trying to play with polymorphism and notice something interesting. Not sure if it is related to tool or it is the correct behavior defined in LRM. In following code, I define 3 classes:

  1. A is the base, B extended from A, C extended from B. They all contain a function called showMsg;
  2. In A, showMsg is non-virtual, it has no argument;
  3. In B, showMsg is virtual, it has one argument, but no default;
  4. In C, showMsg is virtual, it has one argument, default to 0;

I expect when I assign c to b, then do “b.showMsg()”, it should call showMsg from C. In other words, I don’t need to provide an argument as the default is 0. However, VCS gives me a compile error. I have to provide an argument to make it work, and in this case the implementation is indeed from C. So it seems polymorphism is for the implementation part, not the argument default?

Another thinking is that maybe VCS should give a compile error when I try to add a default to C::showMsg? Should the prototype (including argument default) be exactly the same for polymorphism?

 module test;
   class A;
      function void showMsg();
         $display("This is A");
      endfunction
   endclass
   class B extends A;
      virtual function void showMsg(int val);
         $display("This is B, val = %0d", val);
      endfunction
   endclass
   class C extends B;
      function void showMsg(int val = 0);
         $display("This is C, val=%0d", val+1);
      endfunction
   endclass
   initial begin
      A a;
      B b;
      C c;
      b = new();
      a = b;
      a.showMsg();
      c = new();
      b = c;
      b.showMsg(); // I get a compile error!
   end
endmodule

In reply to eda2k4:
The LRM says

Virtual method overrides in subclasses shall have matching argument types, identical argument names, identical qualifiers, and identical directions to the prototype. The virtual qualifier is optional in the derived class method declarations. The return type of a virtual function shall be either:

  • a matching type (see 6.22.1)
  • or a derived class type

of the return type of the virtual function in the superclass.It is not necessary to have matching default expressions, but the presence of a default shall match.

When you call b.showMsg, the compiler needs to ensure that the call is legal for any object that might be stored in the class handle b.

However what most tools have actually implemented is to just use the prototype arguments from the base virtual method and ignore any other defaults in overridden methods. See 01584: problems with default arguments in virtual methods - Accellera Mantis

Dave, I have some long time confusions about up-casting and down-casting. Could you help clarify?

Like the code below, I have the same variables of three classes (line 1). At line 2, I create a class C object z, then up-cast twice from C to B, then to A. At line 3, I down-cast A back to C.

Question: at this step, I up-cast twice then down-cast back. Over the whole process, how object/handle changes? Handles x/y/z moves back and forth, which I can understand. How about the object itself? which is created only once. The instantiated object actually contains everything along the whole inheritance hierarchy? If a method is non-virtual, it can potentially has multiple copies of the method; if virtual there is only one copy (the last). During up-casting or down-casting, tool will pick the right implementation based on handle type (class) and virtual/non-virtual. Is my understanding correct?

Sorry if my question comes off as naive.

  1.  A x; B y; C z;
    
  2.  z = new(); y = z; x = y;
    
  3.  $cast(z, x); z.showMsg();
    

In reply to dave_59:

In reply to eda2k4:
A cast to a class variable, or any kind of assignment to a class variable never changes the object; it only copies the class handle into the target class variable. $cast only adds a run-time check to make sure the handle is valid type to be stored in the class variable.

Class methods are never constructed or copied; they are part of the class type, just like static class members. Every class method you define exists regardless of whether they are virtual or non-virtual. The only difference is their visibility when they are referenced.

A class type provides information on how to look up everything in a class object by way of a set of mapping tables. There is a table for each virtual method. When you reference a virtual method with a class variable, the object type of the handle is used to look up which virtual method to call.

In reply to dave_59:

Could someone help explain this behavior difference? Shouldn’t functions that are virtual be always virtual? Does it matter where it is made virtual-parent/child class.

Variant 1.

module test;
   class A;
      virtual function void showMsg(int val);
         $display("This is A");
      endfunction
   endclass
   class B extends A;
       function void showMsg(int val);
         $display("This is B, val = %0d", val);
      endfunction
   endclass
   class C extends B;
      function void showMsg(int val);
         $display("This is C, val=%0d", val+1);
      endfunction
   endclass
   initial begin
      A a;
      B b;
      C c;
      b = new();
      a = b;
     a.showMsg(1);
      c = new();
      b = c;
     b.showMsg(7); 
   end
endmodule

Result-
This is B, val = 1
This is C, val=8

Variant 2:

module test;
   class A;
       function void showMsg(int val);
         $display("This is A");
      endfunction
   endclass
   class B extends A;
       virtual function void showMsg(int val);
         $display("This is B, val = %0d", val);
      endfunction
   endclass
   class C extends B;
      function void showMsg(int val);
         $display("This is C, val=%0d", val+1);
      endfunction
   endclass
   initial begin
      A a;
      B b;
      C c;
      b = new();
      a = b;
     a.showMsg(1);
      c = new();
      b = c;
     b.showMsg(7); 
   end
endmodule

Result -
This is A
This is C, val=8

The only difference between the two is the place where virtual keyword is used. Rest of the method prototype being exactly the same.

In reply to kernalmode1:

In variant 2, A::showMsg is non-virtual. The code calling a.showMsg must behave as non-virtual because is has no knowledge the class will be extended. That means A::showMsg could have a completely different prototype from B::showMsg if you wanted (i.e. a different number of argument). But once B::showMsg is declared virtual, all derived methods are virtual and must have the same prototype.

In reply to dave_59:

Hi Dave,

Does the virtual method lookup not work for variant 2? Isn’t that deduced at compile/elab stage? In this case, the prototypes of all the functions are exactly the same.

In reply to kernalmode1:

It did work when you called the method from the class variable b. See my article about virtual methods.

In reply to dave_59:

Thanks Dave.

I think what I was missing was the second part of this sentence -

It’s important to know that once a method is declared as virtual, it’s always virtual in all derived classes

The "virtual"ity starts from the point it is declared virtual and does not apply backward.