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
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
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
s-asthan.ads which is the same for all architectures, and three
used for all non-VMS operating systems
used for VMS on the Alpha
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.