In the previous tutorial, we created a constructor and destructor for our TTimer class. Next, we extend our class with overloaded operators. Depending on the type of object your class represents, you may want to define an addition/subtraction/multiplication/… operator. In addition, the assignment operator deserves some extra attention as you may want to have a clear control over this operation (e.g., deep copy vs shallow copy). The full source of this tutorial and the previous, can be downloaded from my github-page.
Let us start with the latter: the assignment operator. As with all other operators, it is possible to overload the assignment operator in modern fortran.
1. Assignment (=) operator overloading
When dealing with objects and classes—or extended data-structures in general—, their properties often are (implicit) pointers to the actual data-structure. This brings an interesting source of possible bugs due to shallow copies being made while deep copies are expected (although the problem may be less pronounced in Fortran than it is in Python).
In a fortran object, the assignment of a pointer component (i.e., an explicit pointer variable, or a component which is an object itself) happens via a shallow copy (or pointer assignment). In contrast, for an allocatable component, the assignment operation performs by default a deep copy (i.e., space is allocated, and values are copied). Shallow copies are very useful with regard to quickly creating new handles to the same data-structure. However, if you want to make a true copy, which you can modify without changing the original, then a deep copy is what you want. By implementing assignment overloading for your own classes, you have more control over the actual copying process, and you can make sure you are creating deep copies if those are preferred.
The implementation of overloading for the assignment operator is not too complicated. It requires two lines in your class definition:
type, public :: TTimer private ... contains private procedure, pass(this) :: Copy !< Make a copy of a timer object generic, public :: assignment(=) => Copy !< This is how copy is used. ... end type TTimer
First, you need to define a class method which performs a copy-operation—which in a fit or original though we decided to call “copy” ;-). As you can see this function is private, so it will not be accessible to the user of your class via a call like :
Secondly, you link this class method via the “=>” to the assignment-operator. It is a generic interface, which means the assignment operator could be linked to different functions, of which the relevant one will be determined and used during run-time. This generic is also public (otherwise you would not be able to use it).
The implementation of the class method follows the standard rules of any class method and could look like
pure subroutine Copy(this,from) class(TTimer), intent(inout) :: this class(TTimer), intent(in) :: from this%firstProperty = from%firstProperty ... !make explicit copies of all properties and components ... end subroutine Copy
The “this” object which we passed to our class method is the object on the left side of the assignment operator, while the “from” object is the one on the right side. Note that both objects are defined as “class” and not as “type”. Within the body of this method you are in charge of copying the data from the “from”-object to the “this”-object, giving you control over deep/shallow copying.
In practice the overloaded operator is used as:
type(TTimer):: TimerThis, TimerFrom TimerFrom = TTimer() ! initialization of the timers TimerThis = TTimer() ! (cf., previous tutorial on constructors and destructors) ... ! do stuff with TimerFrom ... TimerThis = TimerFrom ! although you type "=", the overloading causes this to be implemented as-if you wrote ! call TimerThis%copy(TimerFrom)
2. Operator (+,-,*,/,…) overloading
Just as you can overload the assignment operator above, you can also overload all other fortran operators. However, be careful to keep things intuitive. For example, an addition operation on our TTimer class is strange. What would it mean to add one timer to another? How would you subtract one chronometer from another? In contrast, inside our TTimer class we have a list of TTime objects which can be used to represent a date and time, as-well-as a time interval. For the remainder of this tutorial, we will assume the TTime class only represents time-intervals. For such a class, it makes sense to be able to add and subtract time intervals.
Let us start with the basic definition of our TTime-class:
type, public :: TTime private ... ! the properties of the TTime class ... contains private ... ! the methods of the TTime class ... procedure, pass(this) :: copy ! Copy content from other TTime instance, ! private, accessed via the assignment statement procedure, pass(this) :: add ! Add two TTime instances. procedure, pass(this) :: subtract ! subtract two TTime instances. generic, public :: assignment(=) => copy ! This is how copy is used. generic, public :: operator(+) => add ! This is how add is used. generic, public :: operator(-) => subtract ! This is how subtract is used. final :: destructor end type TTime interface TTime module procedure constructor end interface TTime
The TTime class has a constructor and destructor, implemented as we discussed before. The assignment operator is over-loaded as well. The overloading of the “+” and “–” operator follows the same setup as for the assignment operator. First, you define a class method where you will implement the addition or subtraction. Second, you link this class method to the operator as a generic. The main difference with overloading the assignment operator is that you need to use the keyword operator instead of assignment, during the second step. The class methods are private, while the generic link is public. The only thing left to do is to implement the class methods. In case of operator overloading, the class methods are functions.
pure function add(this,that) Result(Total) class(TTime), intent(in) :: this, that Type(TTime) :: total total = TTime() ... ! implementation of the addition of the properties of ! this to the properties of that, and storing them in ! Total ! e.g.: Total%seconds = this%seconds + that%seconds ... end function add
The returned object need to be defined as a type, and the further implementation of the function follows the standard fortran rules. It is important to note that for a function-header like this one, the object to the left of the operator will be the one calling the overloaded operator function, so:
Total = this + that
Total = that + this
This may not seem this important, as we are adding two objects of the same class, but that is not necessarily always the case. Imagine that you want to overload the multiplication operator, such that you could multiply your time-interval with any possible real value. On paper
Δt * 3.5 = 3.5 * Δt
but for the compiler in the left product “this” would be a TTime object and “that” would be a real, while in the right product “this” is the real, and “that” is the TTime object. To deal with such a situation, you need to implement two class methods, which in practice only differ in their header:
pure function MultLeft(this,that) Result(Total) class(TTime), intent(in) :: this real, intent(in) :: that Type(TTime) :: total
pure function MultRight(that, this) Result(Total) class(TTime), intent(in) :: this real, intent(in) :: that Type(TTime) :: total
In the class definition both functions are linked to the operator as
procedure, pass(this) :: MultLeft procedure, pass(this) :: MultRight generic, public :: operator(*) => MultLeft, MultRight
With this in mind, we could also expand our implementation of the “+” and “–” operator, by adding functionality that allows for the addition and subtraction of reals representing time-intervals. Also here, the left and right versions would need to be implemented.
As you can see, modern object oriented fortran provides you all the tools you need to create powerful classes capable of operator overloading using simple and straightforward implementations.
In our next Tutorial, we’ll look into data-hiding and private/public options in fortran classes.