Returning compound types from subprograms¶
Returning a larger data type like a record is different from returning a simple value which usually fits
into a CPU register. By default – and unless explicitly enforced by using dynamically allocated records and
access types – Ada always uses value semantics to return compound types. This means, the caller always
gets data that is valid after the subprogram returns and everything the subprogram has allocated on its
stack becomes invalid. Such data must either be copied to the caller or
must not be allocated on the subprogram’s private stack at all. An option is to create the data in-place,
that is, in a memory space available to both the caller and the called subprogram. Either way, as a user you
normally do not need to care about such semantics. The compiler does this transparently for you and you
can assume that a subprogram returning a record type will return a valid instance of that type
and not some dangling reference or pointer to a memory location which may (or may not) hold valid data.
Enforcing this is not always simple and may involve additional copy operations between caller and callee, and some optimizations can be performed to eliminate such costly operations. Particularly for larger record types.
For the following observations, we assume a compound type in the following form:
1type Point is tagged record
2 X, Y, Z : Long_Float := 0.0;
3end record;
Aggregate notation¶
In aggregate notation, the subprogram implicitly constructs a record of the required type in its return statement.
1-- construct a new Point from a given Point and an offset
2function Move_Point(p : Point; xdelta : Long_Float) return Point is
3begin
4 -- return an intialized Point record. This follows the same
5 -- syntax as p : Point := (...) declarations.
6 return (X => p.X + xdelta, Y => p.Y, Z => p.Z);
7end Move_Point;
Extended return statement¶
This is a feature that was introduced in Ada 2005. It allows to declare and populate a return value in a loop or other block construct.
Using an out parameter¶
Another option and especially useful for procedures that do not allow a return value or for subprograms
that must return multiple results are out parameters. The caller initializes a matching data type and
passes it as out parameter to the subprogram. The subprogram then populates the record and optionally
can return another value (for example, a success or failure code indicating the validity of the returned
data). This method also does not require any copy operations, because the caller must set up the record
receiving the result. An out parameter (and likewise in out parameters) is passed as a reference,
so no copy operation is involved here either. The subprogram can directly write to the provided memory
location holding the target record structure.
1-- construct a new Point from a given Point and an offset
2function Move_Point(p : Point; rslt : out Point; xdelta : Long_Float) return Boolean is
3 success : Boolean := False;
4begin
5 rslt.X := p.X + xdelta;
6 rslt.Y := p.Y;
7 rslt.Z := p.Z;
8 success := True;
9 return (success);
10end Move_Point;
Avoiding excessive copying operations¶
Copying around larger compound types can be time consuming and it is therefore desirable to minimize or
even completely avoid such operations. Modern compilers (particularly C++, Rust, but also Ada) use
strategies and methods commonly known as Return Value Optimization (RVO) or Copy Elision [1].
Such optimizations usually depend on the level of compiler optimization and may not be used for
non-optimized debug builds. For GCC compilers, they will be active beginning at –O1 optimization
level and will be used more aggressively at —O2 or –O3.
The problem¶
Consider a subprogram returning a large record. The record is first allocated on the stack and constructed during the subprogram executes. It then needs to be returned to the calling entity and since the subprogram’s stack is no longer valid after it returns, a copy operation is needed to give the calling entity access to a valid copy of the data. To solve this issue with RVO the compiler may allocate enough space on the stack of the calling entity and the subprogram will then construct the compound type not on its own, but on the caller’s stack. The calling entity can then use the „returned“ data structure (which, in reality, wasn’t returned, but built in-place of the calling entity) without performiny a copy operation. In C++, the standard requires or even guarantees that Copy Elision is performed, the Ada compiler prioritizes code correctness and may not use such techniques in case they would violate correct behavior. [2].