C vs C++, from the author of ZeroMQ



  • Part 1
    Part 2

    I'm not a C or C++ programmer, but I feel I got a lot of value out of these articles. Especially regarding the initialization problematic I myself have struggled with.



  • Coming from the perspective of someone who mostly disagrees (I know of no program I would write in C instead of C++ unless there just isn't a moderately-reasonable C++ toolchain1), the second article only presents one side of the issue. In many cases, C++ abstractions not only have no overhead but can speed things up via the C-ish thing. For example, suppose you want to sort an array. C gives you qsort; C++ gives you std::sort. One of those is significantly faster than the other. (Hint: it's not qsort.)

    1I would use different subsets of C++ in different situations, but never plain C.



  • With C++ you just throw the error. What happens then is not at all obvious:

    int rc = fx ();
    if (rc != 0)
        throw std::exception ();
    

    Well if you throw a parameterless base exception, duh, of course it's not obvious.

    The problem with that is that you have no idea of who and where is going to handle the exception.

    THAT'S THE FUCKING POOOOOINT! You don't care who and how handles the exception, all your code needs to do is signal that an error occurred!

    Is this guy Spolsky in disguise?

    As you fix individual bugs you'll find out that you are replicating almost the same error handling code in many places.

    Wrong. Quite the opposite, you can wrap everything in a catch block and handle the exception. In C-way, you need to call the handler every single time it's encountered.

    If that's the case a special new object state comes into being. It's the 'semi-initialised' state when object has been constructed but init function haven't been called yet. The object (and specifically the destructor) should be modified in such a way as to decently handle the new state. Which in the end means adding new condition to every method.

    Now you say: But that's just a consequence of your artificial restriction of not using exceptions! If exception is thrown in a constructor, C++ runtime cleans the object as appropriate and there is no 'semi-initalised' state whatsoever!

    Well great, even you can see you're being an idiot.

    Let's compare how a C++ programmer would implement a list of objects:

    class person
    {
        int age;
        int weight;
    };
    
    std::list <person*> people;
    

    No sane C++ programmer would use a linked list for a list.

    That's not where it ends though.

    The solution chosen has direct impact on the complexity of the algorithms. In C++ solution erasing an object from the list has O(n) complexity:

    void erase_person (person *ptr)
    {
        for (std::list <person*>::iterator it = people.begin ();
              it != people.end (); ++it)
            if (*it == ptr)
                people.erase (it);
    }
    

    In the C solution, erasure of the object can be done in constant time (simplified version):

    void erase_person (struct person *ptr)
    {
        ptr->next->prev = ptr->prev;
        ptr->prev->next = ptr->next;
        ptr->next = NULL;
        ptr->prev = NULL;
    }
    

    That's fucking gibberish. In first example, you pass a pointer to a data structure inside a node. In the other, you pass a pointer to a node.

    If you pass an iterator to the list as you should, it's O(1).

    EDIT: A lot of people point out that iterator should be used instead of pointer. However, imagine the object is contained in 10 different lists. You would have to pass structure containing 10 iterators around instead of the pointer. Morever, it doesn't solve the encapsulation problem, just moves it elsewhere. Instead of modifying "person" object every time you would want to add it to a new type of container you would have to modify the iterator tuple structure.

    And pray tell, how do you solve that in C? Keep 10 prev and next pointers inside an object?


  • Winner of the 2016 Presidential Election

    Modern C++ (C++11) is way better than C. (Hint: If you write new in C++11 code, you're probably Doing It Wrong.) It's still a shitty language, but at least it's kind of reasonable to write a large application in it.

    All of the problems mentioned in the first article can be solved by using unique_ptr/make_unique and simply not making the stupid decision not to use exceptions. I didn't even bother reading the second one after that.



  • @Maciejasjmj said:

    THAT'S THE FUCKING POOOOOINT! You don't care who and how handles the exception, all your code needs to do is signal that an error occurred!

    What about the idea you need this code to be super stable and never ever in a million years crash. Therefore you can't just allow some part of the code to basically do a GOTO to the error handler. You need to do things in place, where you still have the entire stack and locals in a known state.

    Personally, I think you can handle this by basically throwing away the entire structure where the error occurred and rebuilding it from scratch, but apparently something like that didn't work for him.


  • Winner of the 2016 Presidential Election

    @cartman82 said:

    What about the idea you need this code to be super stable and never ever in a million years crash. Therefore you can't just allow some part of the code to basically do a GOTO to the error handler.

    So you check the return value of every malloc and surround every new with a try-catch block? Yeah, right...


  • BINNED

    while((var = (var*)malloc(s * sizeof(var_t)) == NULL);
    

    🚎



  • In the C solution, erasure of the object can be done in constant time (simplified version):

    void erase_person (struct person *ptr)
    {
       ptr->next->prev = ptr->prev;
       ptr->prev->next = ptr->next;
       ptr->next = NULL;
       ptr->prev = NULL;
    }
    

    Now I'm not a C/C++ programmer, but won't that do a null dereference if you pass it the start or end of the person list?
    Also forces code to explicitely be aware of those start/end delete cases, because otherwise they'd have invalid pointers to things that were 'deleted'.



  • The worst part of the article is the comment software.

    Oh look, a comment tree I don't care about, I'll just click "fold" on the parent to hide it all and quickly get to the next one

    ARE YOU FUCKING KIDDING ME, WHAT KIND OF RETARDED USELESS FEATURE IS THAT?!?!


  • I wonder when @codinghorror will copycat it ...


    Filed under: But comments will clearly be folded by default, arrows.gif


  • BINNED

    I'd say never, because tree layouts are evil etc.

    But... nested quotes work now...



  • @cartman82 said:

    What about the idea you need this code to be super stable and never ever in a million years crash. Therefore you can't just allow some part of the code to basically do a GOTO to the error handler. You need to do things in place, where you still have the entire stack and locals in a known state.

    But exceptions are more than just a GOTO to the error handler. Properly constructed objects (i.e., objects that are in a known state) are destroyed (with their destructors) when they go out of scope, so you can guarantee that the program is left in a known state as the exception propagates upwards through the stack to whereever you finally handle it.

    And if you don't handle the exception at all, your program is terminated in a controlled manner. Rather than having a hard to find bug because somebody forgot to check the return value of parse_some_random_client_input_thing() for the error code eParseErrorMaliciousClientTriesToOwnOurProcess.



  • I suppose the issue is this (excuse my rusty C++, treat it as pseudocode):

    class NetMonitor {
      private:  
        int checking = false;
      public:
        void check() {
          checking = true;
          this->execute_request(); // <- this throws exception
          //...
          checking = false;
          this->finalize_request(); // <- this also throws exception
        }
    }
    
    

    So you catch this exception somewhere downstream. And now you have no idea where it came from and in what state was the original object left (eg. is checking stuck?).

    If you HAVE to keep working, the only solution here is to throw everything away and start anew. But that might not be feasible in some situations.

    If you handled the error locally, you not only wouldn't have to do the costly re-initialization, you'd have access to the local stack and be able to exactly return everything into a supported working state.

    Of course there are downsides to doing it that way too (code duplication tedium, for one), so *shrugs*.



  • C vs C++, might as well be arguing Brainfuck vs INTERCAL. They both suck and you should not use them.



  • That assumes you can handle it locally. And in that case, you can also catch the exception locally as well.
    You can also tell when/why something failed if you have different exceptions, eg ExecutionException and FinalizeException


  • I survived the hour long Uno hand

    What you want seems to be this: http://stackoverflow.com/a/161247/719165

    A database object. To make sure the DB connection is used it must be opened and closed. By using RAII this can be done in the constructor/destructor.



  • @Salamander said:

    That assumes you can handle it locally. And in that case, you can also catch the exception locally as well.You can also tell when/why something failed if you have different exceptions, eg ExecutionException and FinalizeException

    Yeah. And as the guy says, if you're catching exception locally, that's just overhead, without any of the benefit exceptions are designed to give you.



  • @Salamander said:

    Now I'm not a C/C++ programmer, but won't that do a null dereference if you pass it the start or end of the person list?
    1) You can use a circular linked list with a distinguished "end" node and avoid the special cases. (Actually... I'm not quite sure why this isn't more common. Maybe it is and I just don't do enough C to see it, and always see abstractions.)
    2.) "simplified version"



  • I'm not really 100% sure from the example what the problem is (I mean, if it's the fact that the NetMonitor can be "stuck" in the checking = true state -- the NetMonitor you present seems to be designed explicitly to make that a possibility).

    But essentially nothing is preventing you from catching the exception locally if you need to do some local cleanup. It's more that you don't have to do so everywhere (i.e., in stack frames where the automatic cleanup does the right thing -- which IMO is what you should design toward most of the time).

    The other feature is that exceptions are a lot harder to ignore than return values.



  • @cvi said:

    I'm not really 100% sure from the example what the problem is (I mean, if it's the fact that the NetMonitor can be "stuck" in the checking = true state -- the NetMonitor you present seems to be designed explicitly to make that a possibility).

    Obviously, it's a contrived example, designed to expose the possibility. The point is, if you have some state that is in flux during processing, and throw an exception, and you can't just tear down everything and rebuild from scratch, you have a problem.

    @cvi said:

    But essentially nothing is preventing you from catching the exception locally if you need to do some local cleanup. It's more that you don't have to do so everywhere (i.e., in stack frames where the automatic cleanup does the right thing -- which IMO is what you should design toward most of the time).

    The other feature is that exceptions are a lot harder to ignore than return values.

    Points granted.



  • @cartman82 said:

    Obviously, it's a contrived example, designed to expose the possibility. The point is, if you have some state that is in flux during processing, and throw an exception, and you can't just tear down everything and rebuild from scratch, you have a problem.

    Ok, yeah, for sure there are places where returning a value to indicate success/failure is a lot more convenient (and sensible) than throwing an exception. Figuring out when and where to return and error vs throw an exception can be really difficult. (And sometimes there's no good answer because it really depends how the function in question is being used, and there are valid use cases for going either way.)



  • I'll admit I didn't read the whole article.
    I kind of stopped after I disagreed with the first point and scanned the rest.
    His use of exceptions seem like he missed the point of what exceptions are good for. Also I rarely try/throw/catch in the same block for error handling. If I'm throwing an exception it is because it is something I can't handle at that layer.
    And everything I've allocated will be properly cleaned up because that is what the destructor is there for (smart pointers do that magic as well).
    If I'm using C, I need to design that error handling everywhere and usually it ends up with goto statements.

    In part 2:
    erase_person for C++ should be passing in an iterator, then it is just people.erase(person). Oh he poses a different problem in the EDIT. But if it was C, it would still be 10 calls on all those lists, because he simplified function there is fixing a list, so multiplying it by 10 is no different.
    And hypothetical situations like that are kind of difficult because we don't know what problem he is trying to solve (why he needs 10 lists containing the same object and whether a different data structure would make that problem go away).



  • @cvi said:

    Ok, yeah, for sure there are places where returning a value to indicate success/failure is a lot more convenient (and sensible) than throwing an exception.

    You mean you don't do (omitted most things):

       // This could return a bool if this were C, but we only have exceptions in C++
        void Exists(int num) {
           if (inmap.find(num) == inmap.end()) {
              throw DoesNotExist();
           }
           throw NumberExists();
        }
        
        int main() {
           try {
              Exists(42);
           } catch(DoesNotExist) {
              cout << "Sorry, doesn't exist.";
           } catch (NumberExists) {
              cout << "Found it";
           }
        }

  • ♿ (Parody)

    @Nprz said:

    we don't know what problem he is trying to solve

    Sure. He's rationalizing his language choice.



  • Nah, the proper way would be

    bool Exists(int num) {
        if (inmap.find(num) == inmap.end()) {
            throw DoesNotExist(); 
        }
        return true;
    }
    

    🚎


  • BINNED

    Didn't you mean:

    bool Exists(int num) {
        if (inmap.find(num) != inmap.end()) {
            return true;
        }
        throw DoesNotExist(); 
    }
    

    Because you know some people just love those early returns.


  • ♿ (Parody)

    Dude, if you're going to have an if, you need an else:

    bool Exists(int num) {
        if (inmap.find(num) != inmap.end()) {
            return true;
        }
        else {
            throw DoesNotExist(); 
        }
    }
    

  • Discourse touched me in a no-no place

    @cartman82 said:

    So you catch this exception somewhere downstream. And now you have no idea where it came from and in what state was the original object left (eg. is checking stuck?).

    If you HAVE to keep working, the only solution here is to throw everything away and start anew. But that might not be feasible in some situations.

    The main problem is that C++ botched exceptions. One of the main problems is that a sub-program's failure modes — where not handled entirely internally — should be part of the interface specification of that sub-program, and another is that there didn't used to be a common root exception type (std::exception exists but lots of code still doesn't use it). The combination of things makes writing exception handling code in C++ rather difficult. Java and C# don't have that problem; they didn't botch exceptions (though they have quite different attitudes about what handling exceptions right ought to look like). The problems with exception specifications on functions and methods in C++ are symptomatic of how things have been got wrong.

    I have other reasons for wanting to avoid C++ in my own code. Key reasons for me include that it's quite a lot harder to make a stable ABI with C++ than with C, and the fact that C++ code does seem to be rather more bloated. (The last one might not be a general problem, but it's something I've observed in the specific cases I've examined. Something about C++ template expansion seems to encourage it…)


  • Banned

    @cartman82 said:

    What about the idea you need this code to be super stable and never ever in a million years crash.

    Rust to the rescue!

    🚎



  • @Gaska said:

    rust Ada to the rescue!

    FTFM
    🚎
    🚎



  • @boomzilla said:

    Dude, if you're going to have an if, you need an else:

    We can do better!

    bool Exists(int num) {
        throw MetaphysicalException(num);
    }
    


  • @cartman82 said:

    So you catch this exception somewhere downstream. And now you have no idea where it came from and in what state was the original object left (eg. is checking stuck?).

    What you should do is wrap execute_request() and finalize_request() in try-catch blocks and wrap the exception in a NetMonitorException. In a sensible language you'd also have a finally block to simplify matters, but in this case you can probably set checking to false in the catch clause just fine.

    Presto - you leave the object in a known state, and you still don't need to know what exactly you need to do when the whole process fails. NetMonitor deals with its own state, then passes the torch to the executing object to deal with its state, etc, etc.

    Bubbling up exceptions should involve wrapping - it's overhead, sure, but it's the kind of overhead that lets you write sensible code.

    @dkf said:

    One of the main problems is that a sub-program's failure modes — where not handled entirely internally — should be part of the interface specification of that sub-program

    Like Java's checked exceptions? Sometimes they're fine, sometimes they're just annoying. A lot of the time you don't need to handle an exception - OS's default exception handler is also an exception handler, and often aborting the program is the best thing you can do.



  • @asdf said:

    Modern C++ (C++11) is way better than C. (Hint: If you write new in C++11 code, you're probably Doing It Wrong.) It's still a shitty language, but at least it's kind of reasonable to write a large application in it.

    You shouldn't have been writing large applications in C since... uh, Java probably, .NET definitely. C++ was the best of the worst before, but Bjarne still botched it by insisting on backwards compatibility with C - it exposed way too many naughty bare-metal bits for people to get hurt on.

    At least C does one job well - embedded and otherwise performance-heavy stuff. Which is ironic, since for years it's been regarded as the most sluggish language, but well, tempus fugit.



  • I haven't read the entire thread, but there is one major advantage of C over C++ that we have to deal with at work: C++ libraries (at least in VC++ land) can only be linked against using the same version of Visual Studio they were compiled with. Because of this, we have to ship C++ libraries that were compiled with all versions of Visual Studio from 2005 on to cover all our customers.

    A pure C library can be linked to by any version of the VC++ compiler, so lately we've tried to keep new APIs relatively simple and in pure C just to avoid the support and release overhead.



  • @Maciejasjmj said:

    You shouldn't have been writing large applications in C since... uh, Java probably, .NET definitely.

    I think that's too far back. Java before Java 5, and .Net before 2, were awful. (It will take one hell of a draw to get me to program in a mostly-statically-typed language without the ability to write generic containers.) Java 5 wasn't released for almost a decade after the original version (late 2004) and to this day continues to kinda suck as a language, though .Net 2.0 was only four years behind the first release (early 2006). But even for a couple years after that, .Net only satisfied one use case: I want to develop a Windows-only program. Mono 2.0 wasn't released until 2008; we're starting to get pretty recent actually. Even today, if I wanted to write a cross-platform program, I'd have to spend a while playing around with Mono to try to determine how good of an option it is. I think most of the choices suck... I'd be hard pressed to say that if I spent a while looking at the options, I wouldn't wind back up at Qt/C++ or something. (I suspect I wouldn't... but I certainly don't say that with confidence. Other things to consider would be other JVM languages, D, and maybe DART. Not sure how those would fare either.)


  • FoxDev

    There's always Node.js 🚎



  • @EvanED said:

    But even for a couple years after that, .Net only satisfied one use case: I want to develop a Windows-only program.

    Oh right; I forgot the web didn't exist then.



  • So Java 6 (let's say - I have hardly any experience with 5) - 2006, .NET 3.5 - 2007.

    Java, I think, worked fine cross-platform (and if you wanted a cross-platform windowed app you were hosed either way, with C++ too, and pretty much still are), .NET by the time of 3.5 was pretty flawless too.

    So 7 years ago, you had hardly any excuse - even QT had bindings for other languages. Hell, you could even go Delphi, Lazarus was around for a while too.



  • @blakeyrat said:

    Oh right; I forgot the web didn't exist then.

    You're right; my mind is often in the world of local applications. For server-side web dev, from what I could tell at the time, .Net would have been a fine choice. Certainly a finer choice than C++.



  • Python?

    But yes, C is crap, C++ is crap and its alternatives are also generally crap. Such is life. I think D could be good if given an opportunity.

    Looking a the future, the popular platforms I can see working in pretty much every OS 5-10 years from now are HTML5, .net and (maybe) Android, so best start getting used to them.



  • @Maciejasjmj said:

    Java, I think, worked fine cross-platform

    It works, in the sense of do what you tell it. But man do I find that language annoying to use. C++ is annoying to use too, but for different reasons; C++ is annoying to use because it's hard to build good tools for it. Java is annoying because the designers make deliberate decisions to be (in my valuation) annoying. For whatever reason, the latter reason grates a lot more on me. The one I think I (1) write better software in and (2) enjoy using more of those two is C++. C# is much better, though I haven't done too much in it.

    (Though that attitude is changing against C++ as time goes by, mostly as I do more TDD or almost-but-not-quite-TDD in other languages. C++ compilation times can easily make TDD in that language not really feasible.)

    @Maciejasjmj said:

    So 7 years ago, you had hardly any excuse - even QT had bindings for other languages. Hell, you could even go Delphi, Lazarus was around for a while too.
    Like I said, I don't think I would go with Qt/C++... but it's also not clear that I wouldn't. I'd have to do some investigation of the alternatives before I would make a decision.



  • @anonymous234 said:

    Python?
    I've played with PyQt/PySide a bit. I actually meant to put a qualification that I was working on something performance-sensitive in my statement above, but I seem to have dropped it; that would have been explicitly designed to exclude that as an option. :-) (Maybe PyPy can run PyQt stuff and could work? I dunno. Python also has some potential for migrating bottlenecks to Cython/C/C++ too, so maybe it would turn out all right in the end.)



  • I keep trying to read this. But almost all of it is 'I WANT TIGHTLY COUPLED ERRORS! I DONT CARE IF IM EXCEPTIONING WRONG!' so I keep stopping.


  • Banned

    @anonymous234 said:

    I think D could be good if given an opportunity.

    I think D has already failed as a language - mostly thanks to standard library war, which turned away many early comers. I used to have high hopes for D, actually - but now I'm all for Rust, because it's really a giant step forward from C++; but I'm afraid the documentation hell will make Rust share the fate with D. Let's hope not.



  • @cartman82 said:

    @Maciejasjmj said:
    THAT'S THE FUCKING POOOOOINT! You don't care who and how handles the exception, all your code needs to do is signal that an error occurred!

    What about the idea you need this code to be super stable and never ever in a million years crash. Therefore you can't just allow some part of the code to basically do a GOTO to the error handler. You need to do things in place, where you still have the entire stack and locals in a known state.

    If that's the case, then why are you using exceptions in the first place?


  • Discourse touched me in a no-no place

    @s73v3r said:

    If that's the case, then why are you using exceptions in the first place?

    In languages which don't fuck them up completely, they're a good way to handle errors. They're predictable and give good semantics which can be viewed as a kind of Failure monad, which is a minor variation on the well-known Maybe monad. (The Java/C# bust-up over whether exceptions should be a declared part of the interface notwithstanding; each side tends to point to the other and say “look at those people who are wrong” which leads to little understanding of just how close they actually are.)

    C++ implementations do it differently. After all, if something goes wrong you really want to blow the process away immediately, yes? Everyone always wants that! Especially with exact deletion of every allocated piece of data on the way out! Yay!



  • @dkf said:

    After all, if something goes wrong you really want to blow the process away immediately, yes? Everyone always wants that! Especially with exact deletion of every allocated piece of data on the way out! Yay!

    Often, you do. How do you even recover from being unable to read, say, your resource package?

    You can do cleanup in the abort handler, but that's just begging for double fault-style mess.

    Also, boy, did you miss the joke here.



  • @Onyx said:

    Because you know some people just love those early returns.

    I once got chastised in a "code review" for having multiple exit points in a function. Something about it being hard to follow. Apparently having three extra booleans and nesting three more if statements is easier to follow.



  • @Magus said:

    I keep trying to read this. But almost all of it is 'I WANT TIGHTLY COUPLED ERRORS! I DONT CARE IF IM EXCEPTIONING WRONG!' so I keep stopping.

    I have a sneaking suspicion that this guy would have similar complaints if he was using Java or C#.



  • @EvanED said:

    1) You can use a circular linked list with a distinguished "end" node and avoid the special cases. (Actually... I'm not quite sure why this isn't more common. Maybe it is and I just don't do enough C to see it, and always see abstractions.)

    Because a linked list is almost always the wrong datastructure, especially for a modern target CPU.
    The "next" and "prev" items probably aren't in the CPU cache, and might even be paged out.
    As you can't even work out how many you have without walking the whole list, they are ludicrously slow.
    The only theoretical benefit is when you really are inserting and deleting items continually.
    And if you are doing that, there are usually better structures (hash map etc) that offer other benefits.


Log in to reply