Home > Enterprise >  How to implement Go's defer() in C so that it allows declaring vars?
How to implement Go's defer() in C so that it allows declaring vars?

Time:09-27

In this Modern C video there's a trick that allows to postpone execution of a code until the block/scope exits. It's used as follows:

int main()
{
    int foo=0, bar;
    const char *etc = "Some code before defer";

    defer(profile_begin(), profile_end())
    {
        /* Some code, which will be automatically 
         * preceded by call to profile_begin() and
         * followed by run of profile_end().*/
         foo  ;
         bar = 1;
    }

    etc = "Some code after defer";
    foo = bar   1;
}

Implementation from the video:

#define macro_var_line(name) concat(name, __LINE__)
#define defer(start,end) for( \
        int macro_var_line(done) = (start,0); \
        !macro_var_line(done); \
        (macro_var_line(done)  = 1), end)

It's pretty simply implemented. What might be confusing is the macro_var_line(name) macro. Its purpose is to simply ensure that a temporary variable will have a unique, "obfuscated" name by adding current line number to it (of where defer is called).

However the problem is that one cannot pass code to start snippet that declares new variables, because it is pasted in the for() comma operator that already has an int type used (because of int macro_var_line(done) = …). So it's not possible to, eg.:

defer(FILE *f = fopen("log.txt","a "), fclose(f))
{
    fprintf(f,"Some message, f=%p",f);
}

I would want to have such macro, capable of declaring new vars in start snippet. Is it achievable with standard C99, C11 or maybe some GCC extensions?

CodePudding user response:

You can simulate defer by using the __attribute__((cleanup(...))) feature of GCC and Clang. Also see this SO question about freeing a variable.

For instance:

// the following are some utility functions and macros

#define defer(fn) __attribute__((cleanup(fn)))

void cleanup_free(void* p) {
  free(*((void**) p));
}
#define defer_free defer(cleanup_free)

void cleanup_file(FILE** fp) {
  if (*fp == NULL) { return; }
  fclose(*fp);
}
#define defer_file defer(cleanup_file)



// here's our code:

void foo(void) {
  // here's some memory allocation
  defer_free int* arr = malloc(sizeof(int) * 10);
  if (arr == NULL) { return; }

  // some file opening
  defer_file FILE* fp1 = fopen("file1.txt", "rb");
  if (fp1 == NULL) { return; }

  // other file opening
  defer_file FILE* fp2 = fopen("file2.txt", "rb");
  if (fp2 == NULL) { return; }

  // rest of the code
}

CodePudding user response:

You could use a trick from "Smart Template Container for C". See link.

#define c_autovar(declvar, ...) for (declvar, *_c_ii = NULL; !_c_ii;   _c_ii, __VA_ARGS__)

Basically you declare a variable and hijack it's type to form a NULL pointer. This pointer is used as a guard to ensure that the loop is executed only once. Incrementing NULL pointer is likely Undefined Behavior because the standard only allows to form a pointer pointing just after an object and NULL points to no object. However, it's likely run everywhere.

I guess you could get rid of UB by adding a global variable:

int defer_guard;

And setting the guard pointer to a pointer to defer_guard in the increment statement.

extern int defer_guard;

#define c_autovar(declvar, ...)               \
    for (declvar, *_c_ii = NULL;              \
    !_c_ii;                                   \
    _c_ii = (void*)&defer_guard, __VA_ARGS__)

CodePudding user response:

As I expressed in comments, my recommendation is to avoid using such a thing in the first place. Whatever your video might have said or implied, the prevailing opinion among modern C programmers is that macro usage should be minimized. Variable-like macros should generally represent context-independent constant values, and function-like macros are usually better implemented as actual functions. That's not to say that all macro use must be avoided, but most modern C professionals look poorly on complex macros, and your defer() is complex enough to qualify.

Additionally, you do yourself no favors by trying to import the style and idioms of other languages into C. The common idioms of each language become established because they work well for that language, not, generally, because they have inherent intrinsic value. I advise you to learn C and the idioms that C programmers use, as opposed to how to write C code that looks like Go.

With that said, let's consider your defer() macro. You write,

However the problem is that one cannot pass code to start snippet that declares new variables

, but in fact the restriction is stronger than that. Because the macro uses the start argument in a comma expression (start,0), it needs to be an expression itself. Declarations or complete statements of any kind are not allowed. That's only indirectly related to that expression appearing in the first clause of a for statement's control block. (The same applies to the end argument, too.)

It may also be important to note that the macro expands to code that fails evaluate the end expression if execution of the associated statement terminates by branching out of the block via a return or goto statement, or by executing a function that does not return, such as exit() or longjmp(). Additionally, unlike with Go's defer, the end expression is evaluated in full after the provided statement -- no part of it is evaluated before, which might surprise a Go programmer. These are characteristics of the options presented below, too.

If you want to pass only the start and end as macro arguments, and you want to allow declarations to appear in start, then you could do this:

// Option 1
#define defer(start,end) start; for( \
        int macro_var_line(done) = 0; \
        !done; \
        (macro_var_line(done)  = 1), (end))

That moves start out of the for statement in the macro's replacement text, to a position where arbitrary C code may appear. Do note, however, that any variable declarations will then be scoped to the innermost containing block.

If you want to limit the scope of your declarations then there is also this alternative and variations on it, which I find much more straightforward than the original:

// Option 2
#define defer(start, end, body) { start; body end; }

You would use that like so:

defer(FILE *f = fopen("log.txt","a "), fclose(f), // argument list continues ...
    fprintf(f,"Some message, f=%p",f);
);

That is somewhat tuned to your particular example, in that it assumes that the body is given as a sequence of zero or more complete statements (which can include blocks, flow-control statements, etc). As you can see, it also requires the body to be passed as a macro argument instead of appearing after the macro invocation, but I consider that an advantage, because it facilitates recognizing the point where the deferred code kicks in.

  • Related