Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Object-Oriented Facilities in Ada 95


OCT95: Object-Oriented Facilities in Ada 95

David is a compiler writer of long standing. He was a proselytizer of Pascal, worked on the Apex Ada development environment at Rational Corp., and is the author of FTL Modula-2.


After programming in both Ada and C++, I've come to appreciate the power, safety, and maintainability of Ada programs as well as the advantages of C++ object-oriented paradigm. With the Ada 95 standard, I no longer have to choose between the two.

C++ programmers may not realize that the new Ada standard provides object-oriented facilities. In Ada 95, polymorphism is implicit: It is a result of the way you call functions, not how you declare them. Also, there are no "classes"--object types now appear as "Tagged Records" (structures), while subclasses are created using type derivation.

In Example 1, for instance, Point is an old-fashioned, non-object-oriented Ada record from which a type Size has been derived. Size is identical to Point, but it is a different type. If you attempted to assign a Point to a Size, you would get a compilation error. To make the assignment work, you would have to cast the Point to a Size, like so: s:=Size(p);.

Type derivation is very useful for separating objects that are represented the same way but are logically different. In most languages, when building tables in arrays, only pointers can protect against use of a wrong index. In Ada 95, deriving index types from integer achieves the same result. The new standard extends this type-derivation concept to provide full object orientation.

Ada's OO Facilities

Example 2(a) defines a type Object with two varieties of a Move routine and a function that determines the extent (in x and y) of an Object. The functions are primitive operations of the tagged type because they immediately follow the declaration. (Actually, the rule is a little looser, but it's good practice to declare all user-defined primitive operations immediately after the type declaration.)

As Example 2(b) illustrates, Ada supports named parameters. This is useful for procedures with default parameters. The last of the calls in Example 2(b) uses the construct Size'(1,1), the Ada syntax for an aggregate constant. That is, it creates a constant of type Size with field values of 1.

Example 2(c) shows the derivation of a Line type from the Object type. The record type Line contains the fields of Object, together with a new field. The Move and Extent procedures are primitive operations of the Object type because they are defined immediately after the type declaration. The Move routines will work just as well for a Line as for a base Object, but the extent of a Line is different, so you have to override the Extent function.

Class-Wide Objects

Suppose that the implementation of Move needed to determine the extent of an object. If, inside Move, you call Extent(obj), the extent for Object will be called. You want to call the Extent belonging to the type of object actually passed to Move. You do this with a class-wide type; see Example 3(a).

The phrase Object'class demonstrates type conversion. The Object'class type covers all the types in the derivation tree of which Object is a member. Note that Line'class and Object'class represent the same type; there is one class-wide type for any tree of derivations, rather than one for every subtree.

Whenever a parameter in a call to a primitive operation has a class-wide type, as in the call to Extent in Example 3(a), the procedure call becomes a dispatching call and the function associated with the actual type of obj will be called. Therefore, you could write it like Example 3(b), where o_c is a local variable of class-wide type. Because it has a class-wide type and is of indeterminate size until given a value, the call must be initialized at the time of declaration. Any calls that use o_c will be dispatching. (Notice that o_c is a stack-based variable, not a pointer to some object on the heap.)

Of course, you can also declare a parameter to a subroutine to be a class-wide type; in this case, an object of any type in the derivation tree can be passed as a parameter.

Private Types and Child Packages

All of the fields and subprograms I've declared to this point have been public. Ada also supports private fields and subprograms. In Ada, a private object is private to the package in which it is declared, not to the type itself. to make all the fields of Object and Line private, the previous declarations should look like Example 4(a), which is a complete specification package for these types. In addition, a package body would contain implementations for all the procedures declared in the specification. Line is derived directly from Object in both the public and the private parts of the package specification. In fact, you could derive Line from a type derived from Object in the private part, even though Line was declared as derived directly from Object in the public part. This means that the actual hierarchy of derivation, and the internals of the types themselves, can be hidden from the package's clients.

You could declare additional primitive subprograms immediately following the completion of the declarations in the private parts. The subprograms would be private to the package, but primitive on the declared types and so would take part in dispatching when appropriate.

In Ada, it is an error if there is no implementation in the package body for anything declared in the package specification: You never have to hunt for a missing definition when a program fails to link. In some languages, if the parameter profiles in the declaration and definition of a procedure do not match, you will also get an undefined external at link time. In Ada, the problem will be found when you attempt to compile the package body.

However, because private parts of a record are visible only in the package in which they are declared, package bodies can grow very large. Further, any derived type that needs access to those private details must be in the same package as the original declaration. This would also tend to make packages large and produce large recompilations every time a new type was added or any derived type was changed.

Ada 95 overcomes this with the concept of a "child package," which is physically separate from its parent package, but logically part of it.

For example, suppose you want to derive a type for a circle from the Object type, and you need access to the private part of Object. Without touching the Graphical_Objects package, you can declare the type as in Example 4(b). This child package will act as if it were a package nested inside the Graphical_Objects package. Nested packages are common, but in this case, both the specification and the body can be in separate files. A change made in the specification of this child package will not cause recompilation of packages that use only the parent package.

The child package also provides convenient support for third-party libraries. A user can extend a supplied package without the entire source.

Constructors and Destructors

The types I've examined so far do not have constructors or destructors. Types that have the Ada equivalent of constructors and destructors are called "controlled types."

To declare a controlled type, you derive from a predefined type from the standard package System.Finalization. Suppose you want to keep all instances of a type chained together. The code would look like Example 5. For the moment, examine the public part of the specification that derives a new type from the type Controlled, which is imported from the predefined package Finalization. In Ada, all classes that have constructors and destructors are derived from a type in Finalization that is a child package of the System package, which contains information about a particular implementation of Ada and is always implicitly available.

I've declared three procedures: Initialize, called immediately after an object is initialized with default values; Duplicate, called immediately after an object is assigned a new value; and Finalize, called immediately before an object is destroyed.

Consequently, if both a and b are chained objects and you assign a to b (b:=a;), Finalize will first be called for b to finalize the value about to be overwritten. The value is copied, and Duplicate is called to clean it up. The procedures are, of course, called implicitly by the compiler.

Remember that in Ada you can specify a record statically, as illustrated in Example 3 in the last call to move, which passed a constant Size record as a parameter using the construct Size'(1,1). You could also do this for the Object and Line types. In Example 6, for instance, you can omit the type mark (Object') when it is known from the context. In this case, the values would be assigned to the object, and then Initialize would be called. Obviously, Duplicate and Initialize will often be the same (as in Example 6), so you can just rename Duplicate to Initialize. The ability to declare a procedure and define it as a rename of another was added as part of the new standard.

Alternatively, you can give fields in records default values. This is also done before Initialize is called.

Multiple Inheritance

Multiple inheritance is not part of the Ada 95 standard because it is expensive. It can also cause unexpected surprises with dispatching.

However, some benefits of multiple inheritance can be achieved in other ways. One example is "mixins." If you want a list of some object, you mix in the object with a list class to produce a new class that is both a list and the original class.

Even in C++, this is better done with a template than with multiple inheritance, because types derived from the original type can also be included in the list. With a mixin, only objects inherited from the mixin can be contained in the list. Also, the original type can be held in many different containers.

Another facility that can model some of the semantics of multiple inheritance is an "access discriminant." This allows an object that is a field in a record to access that record. It does not mix the primitive operations of two types together, but this can be made easier with a suitable generic.

Other Ada Additions

While object orientation is the Ada 95 feature that's likely to be of greatest interest to non-Ada programmers, the language has also improved in other ways:

Access types. "Access type" is the Ada term for a pointer. Example 5 uses an access type to reference a variable, as in chn.prev.next. You do not need a special symbol between the fields (as with C and Pascal), so you can change an object with a record type into an object containing a pointer to that record type without editing large amounts of code.

This "locality of maintenance" principle is important in Ada, especially in large projects. The goal is that local changes to code should be both easy and safe. In some cases, this happens automatically. In others, it requires extra work when the code is originally written; for example, using named parameters to subroutines, or avoiding "others" clauses in case statements and array definitions, so that a compilation error will be raised when new arms need to be added.

An alternative to referencing a variable is to create an access variable for a procedure. This is an improvement over Ada 83, where to pass a procedure as a parameter, you created a generic and instantiated it with that procedure as a parameter. (This was fine for Ada code but afforded no standard way to pass callback procedures to APIs like X.)

Finally, some changes support object orientation. You can declare access types to class-wide types (type T is access Object'class). You can also declare parameters to procedures (type access to Object) and have dispatching occur on the actual operand. In other words, dispatching will work for procedures that pass around pointers, as well as procedures that pass the actual objects.

Decimal Types. Ada has always had fixed-point types that allow the expression of fractional numbers without using floating point. A typical declaration looked like this: type hundredths is delta 0.01 range -10.0 .. 10.0, which seems ideal for money calculations.

Unfortunately, the values of this type were represented in binary, essentially as an integer with an implied binary point seven bits from the right. So the value 0.01 was represented as 1/128. From here on, it is steadily downhill if you want results accurate to the penny.

With the new standard, you can declare types that do decimal arithmetic accurately. Instead of the previous declaration, you would write: type Money is delta 0.01 digits 8;, which will perform money calculations correctly. It will hold values up to (but not including) one million dollars. Multiplication of money values can be either truncated (the default) or rounded using the form Money'round(a*b), where a and b can be any decimal types.

Such values can also be formatted using pictures similar to those found in Cobol, making Ada a serious alternative to Cobol for business applications.

Modular Types. Traditional Ada lacked unsigned values that could take on any value allowed by the size of a word. That is, unsigned types could only be a subrange of integer values. With Ada 95, full unsigned types are available. Because their main use is in system programming, they differ from traditional integral types in that:

  • They do not overflow--they wrap around. Adding 1 to the largest value will produce 0, rather than raising an exception.
  • And, Or, and Xor are defined so that bit operations can be performed. Ada has always had other facilities to do bit operations. For example, you could map record structures to bit-exact addresses to directly access hardware-control registers. Even so, these new types are a useful addition.
Generic formal packages. Ada has always had a powerful generic capability, but the number of parameters to a generic could grow very long when a generic was used to extend the operations on a type created by instantiating another generic. You sometimes ended up passing as a parameter every subroutine exported from the first generic into the second.

Ada 95 allows a generic package to be passed as a parameter to another generic. When this second generic is instantiated, the name of an instantiation of the first generic is supplied for the parameter.

New tasking constructs. Ada has always had tasking built into the language. This makes it attractive for applications that need to take advantage of multithreaded operating systems. The original tasking was rather cumbersome, but a new "protected type" simplifies control of concurrency in tasking programs. Although it has its own syntax, it is essentially a package; it has a specification and a body.

In a package, any number of procedures and functions can be executed simultaneously by different tasks. In a protected type, one procedure must complete before another can be entered, so any task that tries to make such a call will be queued until the procedure can be entered. Any number of functions in the protected type can be active at once, but a function cannot be active while a procedure is active. Hence, protected types allow us to implement the classic "n readers, 1 writer" protection mechanism.

The second facility is the ability to execute code that is to be aborted, either after a given interval or when some other event occurs. Example 7, for instance, spends ten seconds trying to prove Fermat's last theorem, then aborts and grumbles about the tightness of the margin.

Conclusion

This article covers only the highlights of the new standard. There are also detailed interfaces to other languages, including C, Fortran, and Cobol. Other annexes include distributed computing, real time, systems programming, and numerics for scientific programming.

References

Barnes, J.G.P. Programming in Ada, Fourth Edition. Reading, MA: Addison-Wesley, 1994.

GNU Ada compiler (GNAT). Available from cs.nyu.edu /pub/gnat. http://lgl-www.epfl.ch/Ada/.

Information Technology: Programming Language Ada, ISO/IEC 8652:1995.

Netnews conference on Ada.comp.lang.ada.

Example 1: An old-fashioned Ada record (not object oriented).

type Point is record
    x : integer;
    y : integer;
    end record;
type Size is new Point;
Example 2: (a) Defining a type Object; (b) supporting named parameters; (c) deriving a Line type from the Object type.
(a)
- Base type for objects desplayed on screen

type Object is tagged record

pos:Point;

end record;

procedure Move(Offset:Size;obj:in out Object);

procedure Move(x : integer :=0;y : integer:=0;obj: in out Object);

function Extent(obj:Object) return Size;



(b)
origin : Object;

...

Move(1,1,origin);

Move(obj=>origin,x=>1,y=>1);

Move(obj=>origin,y=>1);  - x defaults to 0

Move(Size'(1,1),origin);



(c)
type Line is new Object with record

offset:Size;

end record;

function Extent(obj:Object) return Size;
Example 3: (a) Class-wide type; (b) an alternative to (a).
(a)
procedure Move (Offset:Size;obj:in out Object) is

s : Size := Extent(Object'class(obj));

....



(b)
procedure Move (Offset:Size;obj:in out Object) is

o_c: Object'class:=obj;

s : Size := Extent(o_c);

...
Example 4: (a) A specification package; (b) declaring a child package.
(a)
package Graphical_Objects is

- declarations of Point and Size omitted

type Object is tagged private;

- procedure declarations here

type Line is new Object with private;

- Overriding definition of Extent here

private

type Object is tagged record

pos:Position;

end record;

type Line is new Object with record

offset : Size;

end record;

end Graphical_Objects;



(b)
package Graphical_Objects.Conics is

type Circle is new Object with private;

private

type Circle is new Object with record

radius:integer;

end record;

end Graphical_Objects.Conics;
Example 5: Declaring a controlled type.
with Finalization;
package Chained_Objects is
    type Chained_Object is new Finalization.Controlled with private;
    procedure Initialize(Object:in out Chained_Object);
    procedure Duplicate (Object:in out Chained_Object);
    procedure Finalize  (Object:in out Chained_Object);
private
    type Chain;
    type Chain_Ptr is access Chain;
    type Chain is record
            next,prev:Chained_Ptr;
    end record;
    type Chained_Object is new Finalization.Controlled with record
            The_Chain: aliased Chain;
    end record;
end Chained_Objects;
package body Chained Objects is
    head : Chain_Ptr:=null;  - global variables
    tail   : Chain_Ptr:=null;
procedure Initialize(Object:in out Chained_Object) is
    chn:chain renames object.the_chain;
begin
    chn.next:=null;
    chn.prev:=tail;
    if tail/=null then
        tail.next:=chn'access;
    else
        head:=chn'access;
    end if;
    tail:=chn'access;
    end Initialize;
procedure Duplicate(Object:in out Chained_Object) renames Initialize;
procedure Finalize(Object:in out Chained_Object) is
    chn:chain renames object.the_chain;
begin
    if chn.prev=null then
        head:=chn.next;
    else
        chn.prev.next:=chn.next;
    end if;
    if chn.next=null then
        tail:=chn.prev;
    else
        chn.next.prev:=chn.prev;
        end if;
end Finalize;
end Chained_Object;
Example 6: Omitting the type mark (Object') when it is known from the context.
O:Object:=(Pos=>(1,1));
L:Line:=(O with offset=>(1,1));
Example 7: Code that will abort unless some other event occurs.
select
   delay 10.0;
   Put_Line("Margin too small");
then abort
   Prove_Fermats_Last_Theorem;
end select;


Copyright © 1995, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.