Is Ada a better C?

Gavin Smyth

This is an article which appeared in EXE magazine in May 1997, under the slightly modified title of "Ada better than C++?", comparing C++ and Ada language facilities under DOS.

The code associated with the article is also available.


When moving beyond C, C++ tends to be the language of choice for current industrial programming, but Ada could be a more robust alternative

My first encounter with Ada was about nine years ago, and I found it a very large cumbersome language to use - mainly, I think, because I was trying to employ it on a problem for which it was not particularly suited: the application was intended to run under the X Window system, and Ada and X did not get along at all well. At the same time, C++ was starting to make its presence felt, and seemed to be a much slicker way to move beyond the strictures of C.

Almost a decade down the line, I am reconsidering... C++ has become a much larger language than I anticipated; Ada has had a recent rejuvenation (to become the version referred to as "Ada 95" or "Ada 9x"); and I have had a lot more experience of "real world" programming. I now think that for many problems, Ada may well be my first choice instead of C++, and I will try to explain why in this article. Of course, one should always match the language to the problem at hand - I definitely do not maintain that there is a single programming language suited to all tasks, but I suggest that Ada may be worth some consideration.

It must be said that the major factor that stimulated my current interest in Ada was the availability of a low-cost, high quality compiler that runs under DOS. In an earlier article (Go-faster sprites, EXE 11(3), 1996) I discussed DJGPP, an excellent port of the GNU C++ compiler for DOS: there is a GNU based Ada compiler, GNAT (GNU NYU - New York University - Ada Translator, now supported and maintained by ACT - Ada Core Technologies), which happens to be useable with DJGPP. GNAT also runs on various unixes, including Linux of course, and Windows 95/NT. See Useful links for Internet addresses of GNAT and associated resources.

Common criticisms of Ada

People often discount Ada because it is perceived to be large and inefficient. I was guilty of ignoring the language too, until I finally felt that C++ was becoming too complex. Here are some of the common beliefs and misconceptions about Ada:

The Ada language is very large - yes, that is true. However, the formal language definition (over 500 pages) encompasses areas regarded as standard libraries (i.e., outside the language definition in the C/C++ world, so it is not much bigger than either of those languages when support libraries are taken into account. It also looks very verbose to C++ programmers with, for example, "begin" instead of "{" (open brace) but then there is no danger of mistyping and misreading a "begin" as a "(" (open parenthesis).

Ada is difficult to learn - certainly, if you are moving from C to C++, it seems that the steps are small. However, coming at C++ or Ada from scratch, there may not be as much of a difference, though I think it is true that you need to know more Ada to write your first Ada program than C++ to write your first C++ program. One extra thing to take into account is programming conventions - there are a lot of C++ informal rules such as the good practice of validating function parameters, for example using the assert macro - you get that for "free" with Ada. Another metric for illustrating the size of this formal knowledge is the number of books on the subject of good C++ style, compared to a single style definition for Ada programming. Of course, maybe this reflects the much smaller readership for Ada books, or you could argue that a rigid style definition reduces programmer creativity - although I disagree with that statement, there is no need to blindly follow the standard anyway.

Ada compilers are expensive - GNAT is made freely available (though you will get better support, and more up to date releases of the tool set, if you pay).

Ada is not object oriented - this used to be true, but is no longer the case (see later).

Ada is inefficient - it used to be difficult to do low level programming (for example: pointers were not very flexible; and interfacing with other languages, such as assembler, was well nigh impossible) but once again, this is no longer the case. Another criticism is that, say, complete range checking is expensive: however, compile time analysis can automatically remove redundant checks, and GNAT permits many of the checks to be disabled via command line switches. It is still true that in providing a common definition of, say, tasking, the language cannot exactly match a particular architecture, but that will be true of any portable system, and can be worked around in specific cases regardless of the language used.

Lack of library support - there are more C++ libraries in existence than Ada bindings. I am not sure if this is partly because the Ada definition already includes a lot of things for which C++ libraries have to be written, such as tasking or numeric processing. However, there are certainly areas where C++ gets libraries first, such as links to windowing systems: libraries for interfacing to the X Window system, or Microsoft Windows are definitely available for C(++) first since the systems themselves were written mainly in C, and Ada bindings follow later, if at all.

Ada is only for military applications - well, the American DoD was the driving force behind Ada, but so what? Forth was developed to control astronomical telescopes but is can be a useful little language in many areas. Ada was designed to have general purpose applicability and the only real worry is that the language dies leaving you with a huge amount of unsupportable code sometime in the future. The sheer amount of C in existence assures its long term future in the same way as Cobol lumbers on. I believe that Ada has as much of a life expectancy as C++ although I will be the first to admit that I have no hard evidence to support this.

The evolution of Ada

You may be familiar with the initial Ada definition - this was quite restricted: for example, although it included generics (a bit like C++ templates), it did not support any useful form of derived classes; and although tasking was part of the language definition, the mechanism was very cumbersome and people were likely to write their own lightweight threads if they required such facilities. This version of the language is generally referred to as Ada 83 these days, with the coming into existence of a newer definition, Ada 95. Ada 95 extends the older language in a number of ways:

Many of these features have been provided in the past by different vendors, in different ways, but now they are standardised as part of the language.

Comparison of some features with C++

I could spend a large number of tedious pages listing all the differences between Ada and C++, but instead I choose to concentrate on the small number of areas which I consider the most fundamental for the sort of systems I tend to program - real time, multi-threaded programs that need to make efficient use of limited resources. Let me also state that I am not an advocate of either language: both have advantages and disadvantages and either should be used, or even mixed, where appropriate.

First of all, I will look at what I consider are the more useful aspects of object orientation in the two languages, inheritance and dynamic dispatching. C++ classes are, roughly speaking, known as tagged records in Ada. Figure 1 illustrates a short C++ example: three classes with a single virtual function (with a lot of good programming practices thrown out the window for brevity). As expected, this prints out the two lines "I am a B" and "I am a C."

Figure 1: C++ class example

#include <iostream.h>

class A
{
public:
  virtual void print() { cout << "I am an A\n"; }
};

class B: public A
{
public:
  void print() { cout << "I am a B\n"; }
};

class C: public A
{
public:
  void print() { cout << "I am a C\n"; }
};

void printIt( A& a )
{
  a.print();
}

int main()
{
  B b;
  C c;

  printIt( b );
  printIt( c );

  return 0;
}

Figure 2 shows the equivalent in Ada (with the minor change that I have not used pointers). So what are the differences? The Ada program is a lot longer - for a start there has to be an explicit public interface definition in a package specification for the overridable functions, so I chose to create classes.ads, the equivalent of a C++ header file. (I could get away without this file for the C++ example because it was so trivial.) Even taking this "include file" out of the equation, the Ada program is 90 or so "words" versus C++'s 75. I like the way C++ dynamic dispatchable functions are explicitly labelled as virtual, and feel uneasy with Ada's apparent deduction of that fact. I also feel more comfortable with C++ treating the class parameter differently to other parameters (putting it in front of a "." or "->").

Figure 2: Ada class example
File classes.ads

package Classes is

  type A is tagged null record;

  procedure Print( Aa: in A );

  type B is new A with null record;

  procedure Print( Bb: in B );

  type C is new A with null record;

  procedure Print( Cc: in C );

end Classes;
File classes.adb

with Text_IO; use Text_IO;

package body Classes is

  procedure Print( Aa: in A ) is
  begin
    Put( "I am an A" );
    New_Line;
  end Print;

  procedure Print( Bb: in B ) is
  begin
    Put( "I am a B" );
    New_Line;
  end Print;

  procedure Print( Cc: in C ) is
  begin
    Put( "I am a C" );
    New_Line;
  end Print;

end;
File main.adb

with Classes; use Classes;

procedure Main is

  procedure Print_It( Aa: in A'Class ) is
  begin
    Print( Aa );
  end Print_It;

  Bb: B;
  Cc: C;

begin -- Main procedure
  Print_It( Bb );
  Print_It( Cc );
end Main;

This small example does not show off much of either language's object oriented facilities. Ada implicitly has run-time type information (RTTI), and can optimise dynamic dispatching to static linking. C++ now has optional RTTI, and in theory could perform a similar static analysis. It may be useful to be able to avoid the overhead of RTTI in some situations, but it is a valuable debugging aid in complex systems. Ada does not support multiple inheritance, something I have thus far found little use for in C++. C++ has a simple to use constructor/destructor mechanism (well, simple to use until you try to make it interact with inheritance trees, when you usually have to remember to make the destructor virtual; or until you use exception handling, when you have to take care to avoid resource leaks in the two routines). Ada, on the other hand, encourages explicit create/delete functions to be called: there are two exceptions - package bodies can include some start-up code (a bit like static variables' constructors in C++) and types can be derived from the Ada.Finalization package, which does offer something like constructors/destructors, though it does not appear to be as flexible. In summary, there seems little to choose between the languages for practical object orientation.

Another area that I am interested in is low-level machine access: I have needed to write interrupt handlers and, for efficiency, write most of my interrupt service routines (ISRs) in assembler, but wanted to interact with the handlers from high level code. (Ada does include some interrupt handler support, but I have not yet explored it as I am not convinced that high level languages are good enough for some of the ISRs I need to define.) Generally, I make the interaction between the ISR and the foreground code be a shared static variable - in C or C++, this is very easy, just reference the variable as extern volatile. In Ada 83, this would be impossible without stepping outside the language definition, but Ada 95 makes it as easy as C: just declare the variable as a native Ada one, and then use language pragmas to import it from elsewhere and mark it as volatile. Figure 3 illustrates the C++ and Ada ways of referencing an assembler variable (assuming a suitable definition of Byte in the Ada code).

Figure 3: Interfacing with other languages
Assembler variable (GNU syntax) _myVar: .byte 0
C++ import extern volatile unsigned char myVar;
Ada import

My_Var: Byte;
Pragma Import( Assembler, My_Var, "myVar" );
Pragma Volatile( My_Var );

Ada's import pragma supports a number of languages: the language specification determines some level of translation - for example, Fortran matrix variables are treated as row major instead of column major. Similar pragmas may be used to call routines in different languages, or to make Ada variables and routines available to code written in other languages, including of course, assembler - Figure 4 is a short assembly routine that adds two to its integer argument and a bit of Ada code that makes use of it. The Ada 95 specification includes scope for inline assembly code. An interesting feature of GNAT is that it can directly use C++ objects, so if you really want to, you could attempt to use that huge C++ library you have with Ada code!

Figure 4: Importing a routine
Assembler source code

.globl _addtwo
_addtwo:
  movl 4(%esp),%eax
  addl $2,%eax
  ret
Ada example using this assembler routine

with Text_IO; use Text_IO;

procedure Asmbit is

  package Int_IO is new Integer_IO( Integer );  use Int_IO;

  function Add_Two( Val: in Integer ) return Integer;
  pragma Import( Assembler, Add_Two, "addtwo" );

begin -- main "Asmbit" procedure
  Put( "Add_Two( 34 ) is " );
  Put( Add_Two( 34 ) );
  New_Line;
end Asmbit;

In complex real time systems, there are always multiple threads of execution. In C++ (or C), this is supported via a separate library of routines. Ada includes tasking support as part of the language definition, so multi-threaded code is much more portable between systems, assuming that the facilities provided by the language are sufficient. Ada 83 had only one task synchronisation mechanism, the rendezvous - because of its generality, it was fairly heavyweight: Ada 95 has a few other inter-task communications methods, one of which I will now describe. Figure 5 is a trivial example of a two task system, each task printing out lines containing pairs of characters.

Figure 5: Simple tasking example

with Text_IO; use Text_IO;

procedure Tasks is

  task type Char_Printer( Ch: Character );

  procedure Print_Two( Ch: in Character ) is
  begin
    Put( Ch );
    delay 0.1;
    Put( Ch );
    New_Line;
  end Print_Two;

  task body Char_Printer is
  begin
    for I in 1..10 loop
      Print_Two( Ch );
    end loop;
  end Char_Printer;

  Task1: Char_Printer( '*' );
  Task2: Char_Printer( '+' );

begin -- main "Tasks" routine
  null; -- Nothing to do here!
end Tasks;

If you run this under a multitasking environment (Windows 95, or Linux, for example), you will see that the output is somewhat jumbled: instead of nice tidy lines of "++" and "**" you will almost certainly get things like "+**" or just "+" - this is because the two tasks are not synchronised in any way, and the delay inserted between the two character prints exacerbates the problem. Figure 6 shows how Ada's protected types can be used to enforce mutual exclusion: this time, the printing is performed as part of the protected type Printer, which allows only one thread of execution at a time to use its facilities, meaning that each of the two tasks is able to print both of its characters (and the new line) without interference from the other.

Figure 6: Synchronised tasks

with Text_IO; use Text_IO;

procedure Tasks is

  task type Char_Printer( Ch: Character );

  protected Printer is
    procedure Print_Two( Ch: in Character );
  end Printer;

  protected body Printer is
    procedure Print_Two( Ch: in Character ) is
    begin
      Put( Ch );
      delay 0.1;
      Put( Ch );
      New_Line;
    end Print_Two;
  end Printer;

  task body Char_Printer is
  begin
    for I in 1..10 loop
      Printer.Print_Two( Ch );
    end loop;
  end Char_Printer;

  Task1: Char_Printer( '*' );
  Task2: Char_Printer( '+' );

begin -- main "Tasks" routine
  null; -- Nothing to do here!
end Tasks;

GNAT does implements Ada tasking in its DOS variant - multi-threading support is not easy to find in the DOS world so this is good news: however, tasking support is not as well developed as in the other environments. The programs in Figures 5 and 6 both show that although there are two tasks, the first one to start runs to completion before the other gets a look in - there is no rescheduling at the delay statement. The only method I have found so far to cause "fairer" scheduling is to employ heavyweight rendezvous. In Figure 7, the protected type has been replaced with a printer task which waits for a rendezvous from client tasks.

Figure 7: Using a rendezvous to ensure multi-threading under DOS

with Text_IO; use Text_IO;

procedure Tasks is

  task type Char_Printer( Ch: Character );

  task Printer is
    entry Print_Two( Ch: in Character );
  end Printer;

  task body Printer is
  begin
    loop
      select
        accept Print_Two( Ch: in Character )
        do
          Put( Ch );
          delay 0.1;
          Put( Ch );
          New_Line;
        end Print_Two;
      or
        terminate;
      end select;
    end loop;
  end Printer;

  task body Char_Printer is
  begin
    for I in 1..10 loop
      Printer.Print_Two( Ch );
    end loop;
  end Char_Printer;

  Task1: Char_Printer( '*' );
  Task2: Char_Printer( '+' );

begin -- main "Tasks " routine
  null; -- Nothing to do here!
end Tasks;

The rendezvous forces a task switch (from whichever of the Task1 and Task2 is active to Printer) and on completion of the print operation, another task switch runs the previously inactive Char_Printer. This is not really very tidy, but it does illustrate some level of tasking under DOS. Ada has many more tasking facilities, and these are available in all environments (though of limited value under DOS), helping to make multi-threaded code more portable. Of course, there are other difficulties: the task scheduler is not specified in detail, and handling task priorities is not well defined.

In Ada, functions are defined to be free of side-effects and therefore cannot return changes in their arguments. So far the only problem I have found with this is interfacing to C functions which do just that, such as many of the DJGPP DPMI functions which use the return value to indicate success of the operation. There are compiler independent ways around this problem: just declare the function as a procedure therefore ignoring any error return; write an extra layer of wrapper code; or declare the argument as an access type (i.e., pointer) instead of a reference (so that, while the pointer cannot be changed, the thing being pointed to can). I find none of these approaches entirely satisfactory: however, GNAT does support an extra pragma to map such a function on to an Ada procedure call with the first parameter being filled in with the result. Incidentally, it is possible to import a C function as both a procedure and a function with the same name, for the cases when you want to use the return value or when you want to ignore it - this is a level of overloading that C++ does not support, but then it does not need to, since the return value of any function can simply be ignored.

I make a lot of use of the C assert macro, mainly for validating function parameters. C++ does not perform much in the way of numeric range checking - there is no way to declare a type which just takes values in the range 1 to 10 short of going to the effort of defining a class. In Ada it is easy, and you get the checks without having to write extra code - I value tool defences against my own stupidity! Furthermore, you can make that integer type totally distinct from other numeric types so that there is no possibility of accidentally misassigning a variable. That can be a nuisance at times, but fortunately Ada 95 provides facilities for casting between associated types. A common C++ habit is to include optional debugging code surrounded by #ifdef...#endif, but a problem with this approach is dealing with the question of when this extra code should be disabled. In any complex system, debugging code remains active for a very long part of the development cycle and may never be disabled at all. This suggests that one might as well acknowledge that particular fact, and rely on tool support for inserting a lot of the common checks. For the more expensive checks, Ada has a Debug pragma which is a little like conditionally included test code. One concern about all these extra checks is the run time cost, but at least GNAT permits all the checks to be disabled.

Thus far I have concentrated on major areas, and aspects where C++ and Ada differ, but what about other reaches of the two languages which are similar?

One area that C++ scores in is library support. Operating system internals and windowing systems have tended to be C oriented and therefore C++ interfaces are common: Ada lags somewhat, but a number of bindings are available.

A fairly significant facet of the language which I have deliberately omitted is the comparison of development environments. GNAT has the usual level of GNU toolset support (such as: emacs for editing, and gdb for debugging) but this feels a bit primitive compared with more mainstream compilers' "visual" IDEs. I have not examined other Ada compiler vendors' offerings, but I doubt if they are as slick as current mainstream C/C++/Java tools on PCs. If you favour the old Turbo environment, there are a few fairly simple IDEs available for Ada under Windows/DOS.

The verdict on Ada

I hope I have shown that Ada is not the costly lumbering monster that many people consider it to be, and that C++ is really bigger than it looks. With the availability GNAT, Ada is affordable by many more people and the existence of high quality documentation and tutorials via the internet (and in a slowly increasing number of books) will help developers become familiar with the language.

I doubt if any single language is truly suitable for all kinds of development, so one important question is: what is Ada good for? Well, first I will answer the opposite question: what is it definitely bad for? I will not be using Ada for quick throwaway hacks - those I can do a lot more efficiently in C or Perl. I will not be using it for "cutting edge" Windows development - although there is a Windows 95/NT binding, it is fairly basic one and cannot handle recent subsystems, such as DirectX (yet). Ada (or, to be more precise, GNAT) is not suitable for very resource limited systems - GNU compilers do tend to produce rather bulky code. However, I intend to apply it to just about everything else, though not necessarily on its own - for example, I have a few rather convoluted C structure definitions which would be tortuous to translate into Ada, but then they were created to be used with heavily optimised assembler anyway. An examination of the language reference manual annexes suggests that, as well as the type of programming I am interested in (mainly real time systems), Ada will be able to address numeric processing (Fortran's forte), information processing (Cobol's domain), and portable distributed systems.

I think my programming benefits from the basic checking available by default, both at compile time and at run time (or maybe, I am making fewer errors because I am concentrating more on a newly acquired language), and the extra portability of tasking will be useful in some projects. My initial concern when approaching Ada was about potential difficulty in expressing low level designs - breaking through the language's strong typing to get code to do what I wanted. However, Ada 95 lets me do most of what I need, and I can always link in some C or assembler for the bits it cannot handle.

In conclusion, therefore, I think Ada is now a language worth adding to your language toolset.

Useful links

The GNAT home page is to be found at http://www.gnat.com, with the UK FTP download mirror ftp://snowwhite.it.brighton.ac.uk/gnat (the DOS version if GNAT is in ftp://snowwhite.it.brighton.ac.uk/gnat/ez2load). ACT also have a European site, at http://www.act-europe.fr.

The DJGPP home page is http://www.delorie.com/djgpp, with UK FTP download mirror ftp://sunsite.doc.ic.ac.uk/packages/simtelnet/gnu/djgpp.

Lovelace, an interactive Ada tutorial is provided at http://www.adahome.com/Tutorials/Lovelace/lovelace.html.

The Ada language reference manual and rationale may be found in GNAT's FTP mirrors under directories rm9x-v5.95 and rationale-ada95 respectively, or online at the Home of the Brave Ada Programmers.

Another valuble UK site is Ada Language UK Ltd.


Gavin's home page | BeesKnees home page

Last modified on 10th October 2002