[DIP idea] out variables

Q. Schroll qs.il.paperinik at gmail.com
Tue Jan 26 01:01:54 UTC 2021


Main goal: Make the `out` parameter storage class live up to 
promises.
In current semantics, `out` is basically `ref` but with 
documented intent. The initialization of the parameter is more 
like a detail.

General Idea
============

The idea of an out variable is one that **must** be passed to a 
function in an `out` parameter position. Basic example:

     int f(out int value);
     int g(int[] value...);
     int h(out int a, out int b);

     out int x;
     // g(x); // illegal: reads x, but x is not yet initialized.
     // h(x, x); // illegal:
         // reads the second x before the initialization of first 
x is complete.
     f(x); // initializes x.

An `out` variable cannot be read until initialized by a function 
call in an `out` parameter position. Since D has exact evaluation 
order, it is easily determined that one usage of `x` initializes 
it and another in the same overall expression reads it (and not 
the other way around):

     out int x, y;
     /*1*/ if (h(x, y) > 0 && x < y) { .. }
     /*2*/ g(f(x), f(y), x, y);

Evaluation order says in /*1*/ that h(x, y) is executed before x 
and y are read for testing `x < y`.
Evaluation order says in /*2*/ that f(x) and f(y) are executed 
before x and y are read for passing them to g.

Also, multiple execution paths can lead to different 
initialization points:

     out int x, y, z;
     if (g(0)) { f(x); f(y); f(z); } else h(x, y);
     // x, y are initialized.
     g(x, y); // okay: x and y initialized on both branches
     g(z); // invalid: z might not be initialized.

It is always possible to initialize `out` variables using an 
ordinary assignment:

     out int x, y, z;
     if (g(0)) { /*as above*/ } else { h(x, y); z = 0; }
     g(z); // valid: z initialized on both branches


Templates
=========

Similar to `ref`, there will be `auto out` which infers `out` 
based on the arguments passed. `auto out` can be combined with 
`ref` (meaning pass by reference always, but if the argument is 
an out value, this is its initialization) and `auto ref` (meaning 
pass by reference if possible, and if the argument is an out 
value, this is its initialization; it cannot be passed by value 
and be initialized).

With __traits(isOut, param) one can test whether `auto out` 
boiled down to `out` or not.

After being (potentially|definitely|?) initialized, `out` 
variables do not trigger `auto out` to become `out`.


In-place `out` Variables
========================

When calling a function with an `out` parameter, instead of 
passing an argument, a fresh variable can be declared instead:

     if (f(out int x) > 0 && x > 0) { g(x); } else { .. }
     if (g(0) && f(out x) > 0) { g(x); } else { .. }

The type of an in-place out variable can be left out, when it can 
be inferred from the called function. [Clearly it can be done in 
some cases and clearly it cannot be in all templates. Exact rules 
TBD.]
In the first else branch, `x` can be used, since regardless 
whether the `f(out int x) > 0 && x > 0` is true or false, 
evaluating it will initialize `x`.
In the second else branch, `x` cannot be used because `x` might 
not be initialized if g(0) is false.
The visibility of in-place out variables is limited to the 
statement they're declared in. For `if` statements this 
encompasses both branches, but for expression statements, it only 
encompasses that expression:

     x = f(out a) + a; // valid
     y = f(out b);
     // y += b; // error, b not visible
     out int c;
     f(c);
     z += c; // valid

One obvious use-case is functions that return a bool value 
indicating success and the result is an `out` parameter. Usually, 
these functions' names begin with try:

     if (tryParseInt(str, out x)) { use(x); }

Another could be unpacking:

     out T x;
     out S y;
     tuple.unpack(x, y);
     // or
     if (tuple.unpack(out a, out b) && condition(a, b)) { .. }

What do you think? Worth it?


More information about the Digitalmars-d mailing list