Live data from Hacker News

GOTOphobia considered harmful in C

blog.joren.ga

41–50 of 319 posts

Re: GOTOphobia considered harmful in C

#41
post #9

There have been a bunch of major security vulnerabilities due to mistakes involving GOTO in C, being used as suggested by the article. Here’s a memorable one: https://www.imperialviolet.org/2014/02/22/applebug.html

“Goto fail” would have been wrong in a non-goto language that use RAII.

The problem was not goto, it was that the else path was also “give up”. In a non goto/RAII/defer language it would have been something like

    If (error)
        Return
        Return
My assumption has been this was some kind of merge error rather being wrong off the bay. Interestingly mandatory indenting or mandatory braces might have stopped this, but then i would have thought -Werror with the dead code warnings would have as well :-/

But again the error was not the goto, and believing it was is the exact problem the article is talking about: people are so opposed to goto they are unable to see real issues (I have seen C code that tries to avoid goto completely and error handling code becomes horrific, far more complex, and far more error prone that just using goto). The problem with goto is that it is very easy to use it unnecessarily, in ways that complicated control flow but don’t actually make things better.

Re: GOTOphobia considered harmful in C

#42
post #13
post #9

There have been a bunch of major security vulnerabilities due to mistakes involving GOTO in C, being used as suggested by the article. Here’s a memorable one: https://www.imperialviolet.org/2014/02/22/applebug.html

I wondered then if this couldn't have been a merge error.

That’s been my assumption as well.

Re: GOTOphobia considered harmful in C

#43
post #36

The C version: int* foo(int bar) { int* return_value = NULL; if (!do_something(bar)) goto error_1; if (!init_stuff(bar)) goto error_2; if (!prepare_stuff(bar)) goto error_3; return_value = do_the_thing(bar); error_3: cleanup_3(); error_2: cleanup_2(); error_1: cleanup_1(); return return_value; } The D version: int* foo(int bar) { scope(exit) cleanup1(); if (!do_something(bar)) return null; scope(exit) cleanup2(); if…

I find that after writing a lot of Go, manually having to defer/scope(exit) is a lot more error prone than just RAII destructors: it’s impossible to forget to defer the destructor.

The trouble with RAII is when it's necessary to unwind the transactions. See the article I linked to.

Re: GOTOphobia considered harmful in C

#44

I agree that in C, the disciplined use of goto is the way to go, but the "ok so far" version could be better written: if (oksofar) { oksofar = something_done = do_something(bar); } if (oksofar) { oksofar = stuff_inited = init_stuff(bar); } and so forth.

Or even oksofar = oksofar && do_something() to do away with the cumbersome ifs.

Re: GOTOphobia considered harmful in C

#45
post #31

The C version: int* foo(int bar) { int* return_value = NULL; if (!do_something(bar)) goto error_1; if (!init_stuff(bar)) goto error_2; if (!prepare_stuff(bar)) goto error_3; return_value = do_the_thing(bar); error_3: cleanup_3(); error_2: cleanup_2(); error_1: cleanup_1(); return return_value; } The D version: int* foo(int bar) { scope(exit) cleanup1(); if (!do_something(bar)) return null; scope(exit) cleanup2(); if…

Do scopes work without linking against the D runtime?

yes, for `scope(exit)`. Here's the assembler generated:

                push    RBP
                mov     RBP,RSP
                sub     RSP,020h
                mov     -020h[RBP],RBX
                mov     -018h[RBP],R12
                mov     -010h[RBP],R13
                mov     -8[RBP],EDI
                mov     EBX,-8[RBP]
                mov     EDI,EBX
                call      _D5test312do_somethingFiZi@PC32
                test    EAX,EAX
                jne     L2F
                xor     R13D,R13D
                mov     EBX,1
                jmp short       L79
    L2F:        mov     EDI,EBX
                call      _D5test310init_stuffFiZi@PC32
                test    EAX,EAX
                jne     L4A
                xor     R13D,R13D
                mov     R12D,4
                mov     EBX,4
                jmp short       L68
    L4A:        mov     -8[RBP],EBX
                mov     EDI,-8[RBP]
                call      _D5test312do_the_thingFiZPi@PC32
                mov     R13,RAX
                mov     R12D,7
                mov     EBX,7
                call      _D5test38cleanup3FZv@PC32
    L68:        call      _D5test38cleanup2FZv@PC32
                cmp     R12D,4
                je      L79
                cmp     R12D,7
                jne     L8B
    L79:        call      _D5test38cleanup1FZv@PC32
                cmp     EBX,1
                je      L8B
                cmp     EBX,4
                je      L8B
                cmp     EBX,7
    L8B:        mov     RAX,R13
                mov     RBX,-020h[RBP]
                mov     R12,-018h[RBP]
                mov     R13,-010h[RBP]
                mov     RSP,RBP
                pop     RBP
                ret

Re: GOTOphobia considered harmful in C

#46

This is fun! The C version; int foo(int v) { // ... int something = 0; switch (v) { case FIRST_CASE: something = 2; goto common1; case SECOND_CASE: something = 7; goto common1; case THIRD_CASE: something = 9; goto common1; common1: /* code common to FIRST, SECOND and THIRD cases */ break; case FOURTH_CASE: something = 10; goto common2; case FIFTH_CASE: something = 42; goto common2; common2: /* code common to FOURTH a…

> The nested functions usually get inlined by the compiler, so there is no cost to them.

This kind of code typically gets written when ‘usually’ isn’t good enough (of course, once you use a compiler, in theory, there are no guarantees; the compiler could compile the inlined-function one with a goto or vice versa, but programmers typically are more concerned about what happens in practice)

The inlined functions also may increase code size and instruction cache pressure.

On the other hand, having a branch less may be beneficial.

Re: GOTOphobia considered harmful in C

#47

The C version: int* foo(int bar) { int* return_value = NULL; if (!do_something(bar)) goto error_1; if (!init_stuff(bar)) goto error_2; if (!prepare_stuff(bar)) goto error_3; return_value = do_the_thing(bar); error_3: cleanup_3(); error_2: cleanup_2(); error_1: cleanup_1(); return return_value; } The D version: int* foo(int bar) { scope(exit) cleanup1(); if (!do_something(bar)) return null; scope(exit) cleanup2(); if…

I have a distaste for all of these examples, which comes from the existence of a side-effecting operation: calling do_something() necessitates the need to call a cleanup function, which means there's some state being changed but hidden behind the internals of these methods. It is really easy to call this incorrectly which says to me it's just a badly-designed API.

In C# the idiomatic way would be to have each of these 3 things be defined in a class using IDisposable, which is similar to D's scope() -- the declaring class gets a cleanup method when the variable goes out of scope, no matter how that happens.

I assume there's some interaction between these classes, but IMHO that should be explicitly defined and so the code would look something like:

    public void foo(int bar) {
        using var something = new Something(bar)
        if (something.do()) {
            using var stuff = new Stuff(bar);
            if (stuff.init()) {
                using var stuff2 = new Stuff2(bar); // two "stuff"s looks dumb but this is example code
                if (stuff2.prepare()) {
                    return do_the_thing(something, stuff, stuff2, bar);
                }
            }
        }

        return null;
    }

There's actually several ways to structure this code which would result in something that looks better than the above, but being example code and not knowing how `something` and `stuff` interact, it's hard to write this nicely. I'd probably aim for something much more concise like:

    public void foo(int bar) {
        using var something = new Something(bar);
        using var stuff = new Stuff(something);
        using var stuff2 = new Stuff2(stuff);

        return stuff2.prepare() ? do_the_thing(stuff2) : null;
    }

In the above, I assume stuff2.prepare() calls everything it needs to on the dependent objects, but how I'd structure this for real entirely depends on what they're actually doing.

Re: GOTOphobia considered harmful in C

#48
post #36

Earlier quoted context omitted.

I find that after writing a lot of Go, manually having to defer/scope(exit) is a lot more error prone than just RAII destructors: it’s impossible to forget to defer the destructor.

The trouble with RAII is when it's necessary to unwind the transactions. See the article I linked to.

That seems like an inside-out way of doing it to me. I would schedule work onto the transaction struct and make it ultimately responsible for if it should roll back the work or keep it committed.

   let mut tx = Transaction::new();
   dofoo(&mut tx)?;
   dobar(&mut tx)?;
   tx.commit();
There is some overhead to boxing the rollback functions for dofoo/dobar into the transaction object, but it's far less error prone (or maybe you can avoid the boxing by encoding all the rollback operations at the type level: less ergonomic but not by much).

Re: GOTOphobia considered harmful in C

#49
post #9

There have been a bunch of major security vulnerabilities due to mistakes involving GOTO in C, being used as suggested by the article. Here’s a memorable one: https://www.imperialviolet.org/2014/02/22/applebug.html

if considered harmful

Expressions that aren't required to return a value for all possible branches of execution that can also mutate state considered harmful.

Re: GOTOphobia considered harmful in C

#50
I always encourage people to go read Dijkstra's GOTO paper, instead of just its title. It's a short and easy read, almost like a blog post. If you pay attention, you can see that the he was talking about spaghetti code vs structured code. It's better when the lexical structure of the source code maps to the execution structure. That is, if you know what is the current line being executed, you have a good idea of what the global state is, which lines executed before this one, and which will be executed next.
Post reply on HN