In the following code snippet we
have one guarded block and three deferred statements.

guard {
  void * const p = malloc(25);
  if (!p) break;
  defer free(p);

  void * const q = malloc(25);
  if (!q) break;
  defer free(q);

  if (mtx_lock(&mut)==thrd_error) break;
  defer mtx_unlock(&mut);

  // all resources acquired
}

The idea is that we indicate with a defer keyword that the
statement, e.g a call to free, is only to be executed at the end of
the guarded block, and, that we want this action to happen
unconditionally in which way ever the guarded block is left. For the
three deferred statements together it says that they should be
executed in the inverse order than they were encountered. So the
control flow for this code example can be visualized as follows:

http://gustedt.gitlabpages.inria.fr/defer-image.png

Here, the dashed lines represent circumstancial control flow that
might arise when some resources are not available or when the
execution is interrupted by a signal.

This new technique has at least two advantages to commonly used C or
C++ techniques:

proximity
The cleanup code (free or mtx_unlock) is coded
close to the place where its need arises.
visibility
The cleanup code is not hidden in some previously
defined function (such as for atexit handlers) or
constructor/destructor pairs (C++)

For normal control flow (without intermediate return, exit, …)
code with similar properties can be coded with existing tools. The
above is equivalent to something like the following

{
   void * const p = malloc(25);
   if (!p) goto DEFER0;
   if (false) {
     DEFER1:
       free(p);
       goto DEFER0;
   }

   void * const q = malloc(25);
   if (!q) goto DEFER1;

   if (false) {
     DEFER2:
       free(q);
       goto DEFER1;
   }

   if (mtx_lock(&mut)==thrd_error) goto DEFER2;

   if (false) {
     DEFER3:
       mtx_unlock(&mut);
       goto DEFER2;
   }

   // all resources acquired

   goto DEFER3;
   DEFER0:;
}

Here, the if(false) clauses guarantee that the deferred statements
are jumped over when they are first met, and the labels and goto
statements implement the hops from back to front to execute the
deferred statements eventually.

Obviously, most C programmers would not code like this but they
would prefer to write down a linearization of the above, which is a
quite common idiom for cleanup handling in C:

{
   void * const p = malloc(25);
   if (!p) goto DEFER0;

   void*const q = malloc(25);
   if (!q) goto DEFER1;

   if (mtx_lock(&mut)==thrd_error) goto DEFER2;

   // all resources acquired

   mtx_unlock(&mut);

 DEFER2:
   free(q);
 DEFER1:
   free(p);
 DEFER0:;
}

This has the advantage of only making the circumstantial control flow
explicit (with three *=goto=) but it does that for the price of
proximity; the cleanup code is far from the place where its need
arises.

Nevertheless, even this linearization needs some form of naming
convention for the labels. For more complicated code the maintenance
of these jumps can be tricky and prone to errors. This shows another
advantage of the defer approach:

maintainability
The cleanup specification is not dependent on
arbitrary naming such as labels (C) or RAII classes
(C++) and does not change when defer or break
statements are added or removed.

Another important property that is much more difficult to implement in
C (and that needs try/catch blocks in C++) is that all exits
from the guarded block are detected and acted upon: break, return,
thrd_exit, exit, panic, or
an interruption by a signal. That is, unless there are nasal deamons
flying around, we have a forth important property

robustness
Any deferred statement is guaranteed to be executed
eventually.

This is different from C++‘s handling of destructors, which are only
guaranteed to be executed if there is a try/catch block underneath.

This principle of deferred execution extends to nested
guarded blocks in a natural way, even if they are stacked in
different function calls.

Read More

ترك الرد

من فضلك ادخل تعليقك
من فضلك ادخل اسمك هنا