In some cases, none of the approaches described above are adequate. This can occur for example if the set of declarations required is radically different for two different configurations.
In this situation, the official Ada way of dealing with conditionalizing such code is to write separate units for the different cases. As long as this does not result in excessive duplication of code, this can be done without creating maintenance problems. The approach is to share common code as far as possible, and then isolate the code and declarations that are different. Subunits are often a convenient method for breaking out a piece of a unit that is to be conditionalized, with separate files for different versions of the subunit for different targets, where the build script selects the right one to give to the compiler.
As an example, consider a situation where a new feature in Ada 2005 allows something to be done in a really nice way. But your code must be able to compile with an Ada 95 compiler. Conceptually you want to say:
if Ada_2005 then ... neat Ada 2005 code else ... not quite as neat Ada 95 code end if;
where Ada_2005
is a Boolean constant.
But this won’t work when Ada_2005
is set to False
,
since the then
clause will be illegal for an Ada 95 compiler.
(Recall that although such unreachable code would eventually be deleted
by the compiler, it still needs to be legal. If it uses features
introduced in Ada 2005, it will be illegal in Ada 95.)
So instead we write
procedure Insert is separate;
Then we have two files for the subunit Insert
, with the two sets of
code.
If the package containing this is called File_Queries
, then we might
have two files
file_queries-insert-2005.adb
file_queries-insert-95.adb
and the build script renames the appropriate file to file_queries-insert.adb
and then carries out the compilation.
This can also be done with project files’ naming schemes. For example:
for body ("File_Queries.Insert") use "file_queries-insert-2005.ada";
Note also that with project files it is desirable to use a different extension
than ads
/ adb
for alternative versions. Otherwise a naming
conflict may arise through another commonly used feature: to declare as part
of the project a set of directories containing all the sources obeying the
default naming scheme.
The use of alternative units is certainly feasible in all situations,
and for example the Ada part of the GNAT run-time is conditionalized
based on the target architecture using this approach. As a specific example,
consider the implementation of the AST feature in VMS. There is one
spec: s-asthan.ads
which is the same for all architectures, and three
bodies:
s-asthan.adb
used for all non-VMS operating systems
s-asthan-vms-alpha.adb
used for VMS on the Alpha
s-asthan-vms-ia64.adb
used for VMS on the ia64
The dummy version s-asthan.adb
simply raises exceptions noting that
this operating system feature is not available, and the two remaining
versions interface with the corresponding versions of VMS to provide
VMS-compatible AST handling. The GNAT build script knows the architecture
and operating system, and automatically selects the right version,
renaming it if necessary to s-asthan.adb
before the run-time build.
Another style for arranging alternative implementations is through Ada’s
access-to-subprogram facility.
In case some functionality is to be conditionally included,
you can declare an access-to-procedure variable Ref
that is initialized
to designate a ‘do nothing’ procedure, and then invoke Ref.all
when appropriate.
In some library package, set Ref
to Proc'Access
for some
procedure Proc
that performs the relevant processing.
The initialization only occurs if the library package is included in the
program.
The same idea can also be implemented using tagged types and dispatching
calls.