Part of the inherent flexibility in object oriented programming are the concepts of inheritance and polymorphism.
This is the second part in a series of introductory articles on SystemVerilog (SV) object oriented programming (OOP). In the first article, we covered the basics of the class data type and the history of OOP. This article uses examples to explain how to efficiently and correctly use inheritance and polymorphism in preparation for adopting the Universal Verification Methodology (UVM) flow.
OOP is a proven methodology for writing abstract, highly reusable, and highly maintainable software code. Classes are used to model reusable verification environments and the abstract data and methods that operate on them.
Inheritance enables reuse. It’s called inheritance because all the existing properties and methods of an original base (or super) class are passed on to the newly created class, called an extended (or derived) class.
Another key principle of any OOP language is polymorphism. Polymorphism adds the ability to have the same piece of code act differently based on the type of object it is dealing with. First, we will look at inheritance.
Extending class properties and methods
One of the benefits of using inheritance is that any changes in the base class are propagated to all the classes that extend from it. The extended class contains everything declared in the base class plus any additional properties and methods we choose to add. Inheritance also allows us to override existing methods. This flexibility to select and override gives us the power to take existing base classes and customize what we want, leaving known good functionality in place. This has a rippling effect as a base class can be extended into many different extended classes, and those classes can be further extended into many classes as well.
Figure 1 shows a simplified version of a base class, Packet, with three class properties: Command, Status, and Data. We can extend Packet into ErrorPacket with an added Error property. This is almost the same as if we started over with a new class with the Command, Status, Data, and Error properties all in a single class. But by doing it this way we gain all the benefits of inheritance.
We can also add methods to an extended class in the same way we add properties. Overriding a method hides the base method from the extended class, but it does not replace it. This aids reuse because we can make small modifications to the method by adding code around the overridden method. The method being overridden does not have to be in the immediate base class. There can be many layers of extended classes and each extension inherits everything the base class inherited.
Figure 2 Extending class methods.
In Figure 2, the ShowError method has been added to the ErrorPkt class, and we’ve also added a new behavior for the SetStatus() method. This SetStatus method overrides the SetStatus method in the Packet base class. SystemVerilog provides a super prefix to access what would have been accessed had we not overridden it. Super reaches down as many levels as needed to find the overridden method.
All classes need a constructor. Extended classes are no exception, and the first statement in an extended class’ constructor must call its base class constructor. Thus, whenever you have a chain of extended classes, there is a chain of constructors from the base class to the outermost extended class. If no classes have explicit constructors, SystemVerilog inserts implicit constructors along with the calls to the base constructor.
But a problem arises as soon as one of the constructors in the chain has arguments as shown in Figure 3. If you add an argument to a constructor, you will get a compiler error if you extend a class without defining a constructor or if you define a constructor but do not explicitly call the base class constructor. That’s because the implicit call to the base class constructor inserted by SystemVerilog does not know what it should pass as arguments. So, you need to explicitly call the base class constructor passing the required arguments.
So how does inheritance work with class variables and handles? When constructing an extended class, we create a handle to a single object that has all the properties of the base and extended classes. We can set both the Command and Error properties, and we can call the overridden method.
When you assign a class handle to a class variable, the compiler lets you access properties and methods from the perspective of the base class variable type, regardless of which extended object the variable has a handle to.
Figure 4 Calling the printme function.
When calling the function printme in Figure 4, it expects a thing object to be passed as an argument, but you can pass any object extended from thing to the function. However, you cannot access anything defined in the extended class object directly from the base class handle.
Up-casting and down-casting
Constructing an extended class object and using a simple assignment of its handle to a variable of its base class type is known as up-casting. You can never construct a base class object and assign its handle to an extended class variable. However, as a result of up-casting, sometimes you might have a base class variable holding a handle to an extended class object. To access the properties of that extended object, you need to down-cast the handle to an extended class variable. Since we’re not allowed to do this directly, SystemVerilog provides a dynamic $cast function that checks to see if the assignment is compatible.
In the example from Figure 4, we were not allowed to access id from the h variable. In Figure 5, $cast checks if t_h holds a handle to a component object (or any object extended from component) so that we know that it has an id property to access.
Virtual and non-virtual class methods
We’ve seen that we can use inheritance to reuse existing class definitions and extend their behavior. But so far we’ve had to know that we’re dealing with an extended class to access that behavior. This is where polymorphism comes into play.
Polymorphism is the ability to have the same code act differently based on the type of an object its working with. SystemVerilog enables polymorphism in two different ways: statically, at compile time, using parameterized classes and dynamically, at run-time, using virtual methods.
Let’s see what happens when we make a method virtual. Here we have the same Packet class with a SetStatus() method. This time we place the keyword virtual in front of the declaration. Now, instead of the compiler fixing the called method based on the type of class variable, it looks up the virtual method to call based on the type of handle stored in the class variable.
Figure 6 Virtual class methods.
In the run() task shown in Figure 6, we are passing a handle to an object that could be a Packet or an Error Packet; we don’t need to know which. If we passed an ErrorPkt object to the run task when calling p_h.SetStatus(), it would call the SetStatus method defined in ErrorPkt. If we passed a Packet object when calling p_h.SetStatus(), it calls the method defined in Packet. The run task is oblivious to the object type and the method it is calling as long as that object is oblivious or is derived from a Packet class.
To make this work, a virtual method needs to have the same prototype, meaning the same argument type signature, in all derived methods. We couldn’t add an extra argument to SetStatus in the ErrorPacket class and still expect this kind of method call look-up to work.It’s important to know that once a method is declared as virtual, it’s always virtual in all derived classes, meaning you can’t override the virtual nature of a method to make it non-virtual. From the perspective of the base class variable making the call, the prototype of a virtual method is fixed.
A common use of polymorphism, using virtual and non-virtual methods, combines inheritance with deep-copy() along with the construction of a new object to get what’s known as a clone(). A clone() is a virtual method that returns a handle to a new object that is a deep copy of the calling object. And because it’s virtual it does this without knowledge of whether it’s dealing with a base class object or one of its derivatives.
If we look at the clone() method in the base class A, we see that it first constructs an A object, then copies itself by calling the copy method, and then returns a handle to that newly constructed object. The copy() method of A is very simple; it just copies the properties of the object whose handle is passed in as the RHS argument (which stands for the right-hand side of an assignment). The RHS is a handle to the object that called the clone() method. The target is the newly constructed object created by clone().
Figure 7 Extensions to the virtual method clone.
Looking at all the extensions to the virtual method clone in Figure 7, we see that its prototype is always the same – there are no arguments and it returns a handle to an A object. Actually, the only thing different in each clone() override is the type of object it constructs. Each override of copy() replicates its local properties and then calls super.copy() to copy the local properties of the class it was derived from. We want copy() to be non-virtual so that its argument can remain a local class type, which allows direct access to local properties. The virtual nature of the clone method takes us to the proper derived copy() method because we always call copy() from the correct class variable type.
Now we can write a burst task that takes a handle to an A object, clone()s it, and send()s it down a chain of other tasks without ever knowing if it was dealing with an A object or a class derived from A. Base class libraries are typically filled with virtual methods to make your code much more reusable.
Accessibility and abstract classes
Sometimes you want to restrict others from accessing class members as a safety measure to prevent corruption of internal states. Adding a local qualifier to a member makes sure that no one outside the local class can access that member. Adding a protected qualifier to a member also makes sure that no one outside the class can access the member, but it allows extended classes to access the member. Both of these qualifiers help hide some of the implementation details from a user. More likely, they prevent users from relying on implementation details by restricting access and only allowing indirect access in limited ways to those members.
One final construct I want to mention is an abstract class. Abstract classes form the basis of many class libraries by implementing core pieces of functionality; like configuration, reporting, and inter-process communication. Abstract classes also provide an API that makes it easier to integrate class-based models from many different independent sources. This is why you will see many local or protected members inside an abstract class restricting you to the published API. Sometimes that API can require that you provide the implementation for a method, like a clone or print method. An abstract class may declare a prototype of a method and require that you override it with a full implementation. This way the base class library can call a virtual clone or print method of an object and be assured that it you have implemented it in the derived class.
This concludes our introduction to inheritance and polymorphism. In the third article in this series on SV OOP for UVM we will look at how SV supports templates for generic code writing using parameterized classes.
Dave Rich is a senior verification consultant in Mentor Graphics' consulting division.