[M3devel] saving indentation?

Jay K jay.krell at cornell.edu
Sun Sep 2 07:27:55 CEST 2012


Correct, goto is actually an excellent and useful feature,

if you don't have exceptions and/or try/finally and/or destructors.

I use it a lot when I program C. A lot.

It is popular.

Esp. in code that must handle all error cases and never leak.

In sloppy/buggy code, well...





There are approximately the following styles of C and C++.





Let's write a function that copies files, the psuedo code is:    



    FILE* from = fopen(from_path, "rb");    

    FILE* to = fopen(to_path, "wb");    

    const size_t buffer_size = 0x10000;    

    void* buffer = malloc(buffer_size);    

    size_t bytes_read;    

    while (bytes_read = fread(from, buffer, buffer_size))    

    {    

      fwrite(to, buffer, bytes_read);    

    }    

    free(buffer);    

    fclose(to);    

    fclose(from);    





Let's handle all errors and not leak.    





 --  repeated inline cleanup at return





    FILE* from = fopen(from_path, "rb"); 

    if (!from)

       return;

    FILE* to = fopen(to_path, "wb");    

    if (!to)

    {

        fclose(from);

        return;

    }

    const size_t buffer_size = 0x10000;    

    void* buffer = malloc(buffer_size);    

    if (!buffer)

    {

        fclose(to);

        fclose(from);

        return;

    }

    size_t bytes_read;    

    while (bytes_read = fread(from, buffer, buffer_size))    

    {    

      fwrite(to, buffer, bytes_read);    

    }    

    free(buffer);    

    fclose(to);    

    fclose(from);    





I deem this style totally unacceptable.

You end up repeating code.

For N resources, you end up with n + n - 1 + n - 2 + ... 1 code, or n squared.





 -- similar to repeated inline cleanup, but defer checks



    FILE* from = fopen(from_path, "rb"); 

    FILE* to = fopen(to_path, "wb");    

    const size_t buffer_size = 0x10000;    

    void* buffer = malloc(buffer_size);    

    if (!buffer || !to || !from)

    {

        if (!to) fclose(to);

        if (!from) fclose(from);

        return;

    }

    size_t bytes_read;    

    while (bytes_read = fread(from, buffer, buffer_size))    

    {    

      fwrite(to, buffer, bytes_read);    

    }    

    free(buffer);    

    fclose(to);    

    fclose(from);    





This is a bad style.

It has some of the repeation of "repeated inline cleanup".  

Let's say fopen(from) runs out of memory, but fopen(to)

uses less memory and succeeds.

That isn't good -- once out of memory, if you really can't

succeed, best to limit resource usage.



Also, often the later resource allocation depends on the earlier.



That is, this is somewhat of a special case.





 -- fully nested if, cleanup everything just once





    const size_t buffer_size = 0x10000;    

    FILE* from = fopen(from_path, "rb");

    if (from)

    {

        FILE* to = fopen(to_path, "wb");    

        if (to)

        {

            void* buffer = malloc(buffer_size);    

            if (buffer)

            {

                size_t bytes_read;    

                while (bytes_read = fread(from, buffer, buffer_size))    

                {    

                    fwrite(to, buffer, bytes_read);    

                }   

                free(buffer);

            }

            fclose(to);

        }

        fclose(from);

    }



This code 

has no repetition.

However, imagine if you are going to return error_no_memory 

vs. error_open_to_failed vs. error_open_from_failed..then you

have to fill in the elses, and its gets messier.

I already don't like the loss of horizontal space, seriously.





I think this style is also unacceptable.

Some people I know claim it is readable. At least by them.

Sure, it isn't impossible to understand. But it is clearly

not the way to write code that must be read and written by

many people. I can't explain it well. I think it should be obvious

to anyone. But yet people disagree.





 -- try/finally, nested 

    const size_t buffer_size = 0x10000;    

    FILE* from = fopen(from_path, "rb");

    if (from)

    {

        FILE* to = fopen(to_path, "wb");    

        if (to)

        {

            try

            {

                void* buffer = malloc(buffer_size);    

                if (buffer)

                {

                    try

                    {

                        size_t bytes_read;    

                        while (bytes_read = fread(from, buffer, buffer_size))    

                        {    

                            fwrite(to, buffer, bytes_read);    

                        }   

                    }

                    finally

                    {

                        free(buffer);

                    }

                }

                finally

                {

                    fclose(to);

                }

            }

            finally

            {

                fclose(from);

            }

        }





If you have to be exception safe, I guess this is an improvement.

But it is clearly bad (once you keep reading).

Again you might have to fill in "else" to return specific errors.



 -- try/finally no nesting

 

    const size_t buffer_size = 0x10000;    

    void* buffer = 0;

    FILE* to = 0;

    FILE* from = 0;

    try

    {

        from = fopen(from_path, "rb");

        if (!from)

            return error_open_from_failed;

        }

        FILE* to = fopen(to_path, "wb");    

        if (!to)

            return error_open_to_failed;

        

        void* buffer = malloc(buffer_size);    

        if (!buffer)

            return error_out_of_memory;

        size_t bytes_read;    

        while (bytes_read = fread(from, buffer, buffer_size))    

        {    

            fwrite(to, buffer, bytes_read);    

        }   

    }

    finally

    {

        free(buffer);

        if (to)

            fclose(to);

        if (from)

            fclose(from);

    }





This isn't bad. Except, you know, try/finally doesn't exist in standard

C and C++. It exists just fine in Microsoft C.

It sort of exists in standard C++.

And it sort of does and doesn't exist in Microsoft C++.

I will explain.

First, the above is valid Microsoft C, assuming #define try __try, #define finally __finally.



Second, in C++ you can do:

  catch(...)

  {

     cleanup

     throw; // rethrow

  }





In Microsoft C++, which does have __try/__except/__finally, you cannot

use __try/__except/__finally in a function that has locals

with destructors. This is both easy to workaround and solvable

in the compiler/runtime, but not trivial in the compiler/runtime

and almost nobody seems to care.



  

but notice, I don't believe "return is an exception"

so that doesn't help in the above.





You can only have one "frame handler" per frame.

There are separate "frame handlers" for "SEH" (__try/__except/__finally)

than for C++ EH. You can split your function up to workaround it.

The runtime could hypothetically provide one unified handler.

The compiler could hyoptheticaly split your function for you.







Let's move on.



 -- C++ destructors.

 

 struct Buffer_t {

    void* p;

    Buffer_t(void*q = 0) : p(q) { } 

    ~Buffer_t() { free(p);  }  

    void* operator=(void*q) { p = q; return q; }  

    operator void*() { return p; }

 };

 

 

struct File_t {

    FILE* f;

    File_t(FILE*g = 0) : f(g) { } 

    ~File_t() { if (f) fclose(f); }

    FILE* operator=(File_t*g) { f = g;  return g; }  

    operator FILE*() { return f; }

 };





These classes need more filling out, but this is enough for here/now.





    const size_t buffer_size = 0x10000;    

    File_t from = fopen(from_path, "rb");

    if (!from)

        return error_open_from_failed; // or raise an exception

    File_t to = fopen(to_path, "wb");    

    if (!to)

        return error_open_to_failed; // or raise an exception

    Buffer_t buffer = malloc(buffer_size);    

    if (!buffer)

        return error_out_of_memory; // or raise an exception

    size_t bytes_read;    

    while (bytes_read = fread(from, buffer, buffer_size))    

        fwrite(to, buffer, bytes_read);    





This is, essentially, perfect.

You need a library, and you need to constantly evolve it,

but it isn't hard.

You can use early return or raise exceptions.

It becomes very difficult to either fail to initialize

data or to leak -- the cardinal sins of C code are gone.

The default behavior is correct.

It becomes also a bit of work to miss an error check -- if you use exceptions.

You regain all you horizontal space.

You get all the easy cases out of the way right away.

There is no duplication.

It is just great.





Now, let's say we don't have C++ but we have C.



The best you can do is this:



    const size_t buffer_size = 0x10000;    

    FILE* from = { 0 };

    FILE* to = { 0 };

    void* buffer = { 0 };

    

     from = fopen(from_path, "rb");

    if (!from)

    {

        error = error_open_from_failed;

        goto Exit;

    }

    to = fopen(to_path, "wb");    

    if (!to)

    {

        error = error_open_to_failed;

        goto Exit;

    }

    buffer = malloc(buffer_size);    

    if (!buffer)

    {

        error = error_out_of_memory;

        goto Exit;

    }

    size_t bytes_read;    

    while (bytes_read = fread(from, buffer, buffer_size))    

        fwrite(to, buffer, bytes_read);    



Exit:

   free(buffer); /* NULL ok */ 

   if (from) fclose(from);  

   if (to) fclose(to);  

   

   

This gets the easy cases out of the way early.

Doesn't leak.

Doesn't have nested ifs "like crazy".

Doesn't give up horizontal space rapidly.





And please release this is only for 3 resources.

Imagine you have 10.

All the bad cases get much worse.

The acceptable cases remain just as good.





You might say "but break up your code into smaller functions".

But it doesn't take much to give up 80 columns in the indent-heavy

styles.





goto is good.

At least if you don't have try/finally and/or destructors.

  Otherwise goto is indeed rarely needed.



Multiple returns per function are bad, if you don't have

try/finally and/or destructors -- otherwise multiple returns aren't needed.





Modula-3 does have "exit" which is like "break".

But it is lacking "cotinue".





 - Jay





Subject: Re: [M3devel] saving indentation?
From: dragisha at m3w.org
Date: Sat, 1 Sep 2012 23:18:13 +0200
CC: m3devel at elegosoft.com
To: jay.krell at cornell.edu

It is only normal to have such an attitude when coming from "save typing" language/world/mindset. If nothing else, C is economic. :)
Correct me if I am wrong but - continue is hidden goto. When you are attuned to it, it's nice and cozy. When you are not, you look around a lot, scratching your head. break, continue, goto. Just recently a friend explained to me how goto is totally legitimate way to handle exit from a procedure - and he said it is sooo readable.. So is break and continue, when you are in a right groove. I don't think I am :).
Variable declaration everywhere (it reminded me of Javascript for a moment, and I had chills :) - it looks nice but it is obviously not great rogramming style. C9x probably has some solution to "name already defined in this same scope" and similar, but it looks clumsy in any case.
I try not to overuse WITH. From time to time I find local variables or extra block much more readable. Of course, in your case you'll  sacrifice C9x economy - variable setting is not its initialization. But, You'll save some horizontal space and skip few WITH's.
As for horizontal space. You are not kidding here? Or you have some reason to not like widescreen laptops? If there is one thing my macbook  has in abundance - it is horizontal space.
Of course, I am totally on the other side of programming language spectre. My background is Modula-2 (25 yrs) and Modula-3 (cca 15yrs). Everything else is secondary to these two. So, I am probably missing a lot of C9x viewpoint issues.

On Sep 1, 2012, at 10:28 PM, Jay K wrote:I would LIKE to write it like so:


        var-or-const name := M3ID.ToText(proc.name);
        var-or-const length := Text.Length(name);
        FOR i := FIRST(u.handler_name_prefixes) TO LAST(u.handler_name_prefixes) DO
            var-or-const prefix := u.handler_name_prefixes[i];
            var-or-const prefix_length := Text.Length(prefix);
            IF length <= prefix_length OR NOT TextUtils.StartsWith(name, prefix) THEN
                continue;
            END;
            var-or-const end = Text.Sub(name, prefix_length);
            FOR i := 0 TO Text.Length(end) - 1 DO
                IF NOT Text.GetChar(end, i) IN ASCII.Digits THEN
                    RETURN FALSE;
                END;
            END;
            RETURN TRUE;
        END;
        RETURN FALSE;


but Modula-3 doesn't have "continue", right, and var/with imply indentation,
So I have to write:
        WITH    name = M3ID.ToText(proc.name),
                length = Text.Length(name) DO
            FOR i := FIRST(u.handler_name_prefixes) TO LAST(u.handler_name_prefixes) DO
                WITH    prefix = u.handler_name_prefixes[i],
                        prefix_length = Text.Length(prefix) DO
                    IF length > prefix_length AND TextUtils.StartsWith(name, prefix) THEN
                        WITH end = Text.Sub(name, prefix_length) DO
                            FOR i := 0 TO Text.Length(end) - 1 DO
                                IF NOT Text.GetChar(end, i) IN ASCII.Digits THEN
                                    RETURN FALSE;
                                END;
                            END;
                            RETURN TRUE;
                        END;
                    END;
                END;
            END;
        END;
        RETURN FALSE;

 		 	   		  
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://m3lists.elegosoft.com/pipermail/m3devel/attachments/20120902/129371a5/attachment-0002.html>


More information about the M3devel mailing list