Software maintenance

Discussion in 'C Programming' started by jacob navia, Sep 13, 2010.

  1. jacob navia

    jacob navia Guest

    Recently in another thread, we discussed a bit about C vs C++
    maintenance. As usual in those discussions, many of the arguments and
    ideas behind the views expressed weren't even said, and even less dicussed.

    I would like to make the motives behind my viewpoint explicit.

    Object oriented programming (OO for short) took off in the middle of the
    80s. It promised increased modularity through components and reduced
    numebr of lines of code through reuse.

    The reality is quite different however.

    OO legacy code is very difficult to deal with due to acute lack of
    modularity and the additional dependencies introduced by the frameworks
    that OO code needs to get going. Since the frameworks lack in many cases
    a formal API, the code using them becomes completely dependent of the
    framework through inheritance...

    This can be impossible to get rid of later, when either the framework is
    abandoned or (the other extreme) it changes regularly each year and each
    year you have to spend a lot of work just to keep up with it.

    Another big problem is the lack of modularity of OO, because modularity
    relies on explicit and well defined INTERFACES and APIs, that are seldom
    used in OO programming. Everything is tied with everything else, and the
    code becomes a horrible tangle of code, appropiately known as a "tar
    ball" :)
    `
    C++ adds to the problems of code maintenance with its implicit
    overloading of identifiers that makes impossible for the maintenance
    programmer to figure out easily what function is being called when he
    sees a function call or even a simple statement like "a+b". In most
    cases the only sure way to know is to put a breakpoint and try to bring
    the program to that point in the debugger. This doesn't always help
    unless the debugger is available and the work to be done to bring the
    program to that point is not beyond a certain threshold.

    Add to this lethal mix the template problem. Templates give genericity
    to C++ at an enormous cost in maintenance problems. In general, it is
    impossible to debug problems in templates when they are extensively used
    unless the one debugging the application is the original author of the
    whole mess. Any modification even an apparently harmless one can produce
    a cascade of completely incomprehensible error messages in a completely
    unrelated part of the code. If many modifications were done before
    recompiling the whole application this can be incredibly difficult to
    solve. Templates introduce a new sub-language in C++ that is run during
    compile time. It is completely impossible in a complex application to
    test templates with all possible types that they could receive as input.
    Concepts were designed to allow some specifications to be given to
    templates in an explicit form, but they failed (more on this later). We
    are stuck with no specifications for template parameters at all. Testing
    is therefore much more difficult.

    One of the most telling facts about the complexity of C++ was the
    "Concepts" catastrophe. After years of effort, the creator of the
    language realized that he was unable to introduce a new feature and the
    feature was taken out of the new C++ standard to be published shortly.

    This proves that C++ has grown so complex that there isn't any human
    mind capable of understanding it, not even the mind that started the
    language several years ago.

    Now, since C++ is uncomprehgensible in its totality, C++ programmers
    master *some* part of it well and use a subset of it that fits their
    needs and the needs of the company. Problem is, obviously there isn't a
    SINGLE subset that is approved by everyone :)

    Each programmer will use then, the constructs he/she is used to. The
    only looser is the maintenance programmer that needs to understand them ALL.

    Much of my time is spent debugging a C++ mountain of code. Maybe that
    is why my repugnance against C++ and complexity holes is so great, I am
    surely biased. But people here can't deny that maintenance is a BIG part
    of any software activity, and that languages are seldom designed from
    that point of view.

    Many speak about easy of use, meaning the easy to WRITE code. I prefer C
    because easy of use means for me more the ability of READING and
    UNDERSTANDING code in that language.

    Obviously these are my views and engage only myself. I hope we can avoid
    polemic in discussions here. This is not an "all out" attack on C++
    either. It is just a dissenting view about it, and an explanation why I
    prefer C.

    jacob
     
    jacob navia, Sep 13, 2010
    #1
    1. Advertisements

  2. I stopped using C++ some years ago, for many of the same reasons.

    However another reason is that object-oriented design is difficult to
    do. Procedural decomposition is usually quite easy - identify the
    central algorithm, and write it if possible in a general way,
    implement and debug it. Then put the auxiliary functions around it to
    format the input and display the results to the user. Designing a good
    object hierarchy, by contrast, is hard.
     
    Malcolm McLean, Sep 13, 2010
    #2
    1. Advertisements

  3. jacob navia

    Ian Collins Guest

    You, like so many other are starting form the erroneous assumption that
    C++ is an OO language. It is not, it is a multi-paradigm language.
    Its is pretty bloody obvious from the context what's being added!
    Unless the code is written by a former C programmer who writes long
    functions and declares everything at the top.

    Most of the crappy code I've had to fix up (in both C and C++) was
    written by newcomers bringing baggage from another language. It's worse
    with C++ when they want too squeeze in as many new toys in as they can.
    That's were solid mentoring can save the day.
    Have you ever heard of unit tests or test driven development? I only
    spend a fraction of my time debugging (no matter which language I'm
    using), if I mess something up, my tests fail and I revert the change
    and do it correctly.
    If C could do RAII, I'd probably use it more often than I do. That
    feature alone makes code so much cleaner and less prone to bugs. So
    much so I've worked with a team who only used the C subset of C++ plus
    RAII and function overloading.

    Any tool can be abused and any language can be used to write
    incomprehensible code. It is the development process not the language
    that determines the overall quality of the resting application.
     
    Ian Collins, Sep 13, 2010
    #3
  4. [...]

    Good post!


    /August
     
    August Karlstrom, Sep 13, 2010
    #4
  5. and at its best it can achieve the modularity. I'm less convinced by
    the number of lines argument. Desparate attempts at implementation
    reuse by inheritance can be truly horrid.

    See LSP
    as the old saw has it "you can write FORTRAN in any language". Well,
    you can write crap in any language too.
    well written OO code is well modularised and has clean interfaces.
    It's kind of what OO is all about.

    not in my experience. You don't have to have frameworks. If you're
    going to do anything useful in C you're going to need libraries or
    whatever.

    that was rather scarey

    and for the record I have maintained a large C++ code base. And yes
    parts of weren't excellent.
    C++ was explicitly supposed to be suitable for large projects (we can
    agree or not if it succeeded).

    Have you read Lakos?

    Have you heard of the Open-Closed Principle?

    I've seen some pretty poor C in my time. With badly designed
    interfaces and leaky APIs (in the abstraction sense). Global data,
    vast functions, huge "modules".
    I'm not sure you're criticising C++, but particular C++ programs.

    Your points on the difficulties of templates strikes a resonance with
    me. "oh dear they used a template here :-("
     
    Nick Keighley, Sep 13, 2010
    #5
  6. jacob navia

    Felix Palmen Guest

    I think you're right here, only I'd explain it a little differently: C++
    supports object orientation by providing appropriate language
    constructs, but it does not force you to use it. For the term
    "multi-paradigm", I'd expect some functional or declarative constructs
    in a language nowadays.

    On the other hand -- I really hate a lot of existing C++ code because it
    uses this "C with objects" style. If I want to do that (I want it quite
    often), I use C for it.
    I still prefer having all declarations of local variable at the
    beginning of the local scope. If this causes problems understanding the
    context, it's a clear indicator that your function is to long.
    That's correct indeed, but I think the complexity of C++ really adds to
    the risk of ending up with such code, especially when written by people
    without years of C++ experience.

    Regards,
    Felix
     
    Felix Palmen, Sep 13, 2010
    #6
  7. my reasons are mostly that it has a more complex syntax and semantics, and
    mostly that most of what it offers is syntax sugar...

    also, apart from the single-inheritence case, the object system gets messy
    and confusing (I personally prefer the Java/C# style SI+interfaces design
    instead, both for offering a cleaner design and semantics, and also for
    being simpler WRT the implementation).

    exceptions are nice, but are not standard in C, ...


    as for style, I suspect I use a mix of procedural and OO style.
    I may use OO-like strategies when appropriate, ...

    usually I prefer to divide the larger app into "components", which focus on
    problems at a much larger scale than typical OO objects (which are typically
    very small-scale).


    however, the "jerk off the class heirarchy" aspect of OO isn't something I
    particularly relate to.
    yet, many people think of OO, and think of it in terms of some big elaborate
    system for jerking off the class heirarchy and throwing in as many fancy
    sounding words as possible (and trying to mimic the "tree of life" diagrams
    or similar...).


    most of my designs have tended to be more "bottom rooted", with everything
    common generally residing at the bottom of the tree (typically shallow), and
    often with N/A operations being no-op and returning an error status or
    similar.

    often, I do similar in C, although I more typically use "daisy-chaining"
    (where the "sub-classes" are "context dependent pointers") rather than
    physical struct extension or nesting...

    or such...
     
    BGB / cr88192, Sep 13, 2010
    #7
  8. jacob navia

    BartC Guest

    Doesn't lcc-win32 (a C compiler) have the same problem?
     
    BartC, Sep 13, 2010
    #8
  9. jacob navia

    Ian Collins Guest

    Whether OO is difficult to do really depends on the individual. If you
    tend problem solve by breaking the problem down into objects, it is
    easy. if you don't, it's hard. I think in objects and I was writing OO
    style C long before I discovered the infant C++.

    A good object hierarchy, like a good functional decomposition will find
    its self through continual refactoring.
     
    Ian Collins, Sep 13, 2010
    #9
  10. A lot of people confuse "object orientation" with "enacapsualtion".

    In C we can create objects like, say, a balanced binary tree. We can
    have a function to intialise the tree (a constructor), a function to
    add an element, a function to enumerate the elements, a fucntion to
    delate an element, and a function to destroy the tree and free up all
    the memory. In C++ you would naturally call this a "class".

    However you haven't yet got an object-oriented design. Object-oriented
    design comes when someone says "a hash table has many things in common
    with a balanced binary tree, let's abstract out the common elements
    (you can add items, delete items, look up items quickly with a key
    etc), and provide them with a common interface. Then client code can
    be passed either a hash table or a red black tree or an avl tree, it
    won't know or care about the difference."
    You can't do object-oriented design in C without murdering the
    language with tables of function pointers and the like. You can do it
    in C++, that's the main reason it was invented.

    I'd disagree that the difficulty of object-oriented design depends on
    the individual. Of course some people are better at it than others.
    But it's inherently a difficult thing to build up a hierarchy of inter-
    related objects that share common functionality. For instance hash
    tables and red black trees are clearly "containers". So are arrays.
    But is it really wise to make arrays "containers"? It's a very
    difficult question. These issues are constantly thrown up in object-
    oriented design. Once having made a decision, it's hard to go back. I
    have seen good object-oriented designs, but only a few of them.
     
    Malcolm McLean, Sep 13, 2010
    #10
  11. jacob navia

    Felix Palmen Guest

    How is this "murdering the language"? C++ Compilers use a vtable, that's
    not much of a difference (other than some optimizations like having the
    vtable only once per class and leaving methods out that aren't
    overloaded).

    The syntax for calling the member functions [ foo->bar(...) ] also looks
    quite similar.

    Generally, a language providing structured data and procedural style
    constructs is enough to do some OOP. Having function pointers/references
    additionally enables the use of polymorphism.

    Of course, C++ provides much more than these minimum requirements.

    Regards,
    Felix
     
    Felix Palmen, Sep 13, 2010
    #11
  12. You need a vtable, then you need to pass about the objects as void *s,
    then have some scheme for nesting the base class structure, and
    resolving back to the base class vtable.

    It can be done, but there's more to it than just declaring a few
    function pointers.
     
    Malcolm McLean, Sep 13, 2010
    #12
  13. jacob navia

    Felix Palmen Guest

    It's not that hard to hide these things behind a few functions and
    preprocessor macros -- BTDT.
    The effective scheme comes down to just that -- you call your member
    functions through pointers in the struct. How is this "murdering the
    language"?

    Regards, Felix
     
    Felix Palmen, Sep 13, 2010
    #13
  14. A toy example will have just a few function pointers embedded in a
    struct. You can achieve polymorhism of a sort.

    When you move to a full-blown system, you realise that this simple
    system doesn't provide you with the flexibility that you need. You
    need to pass in a void pointer. You then need to query it for the
    interface. You then need to get the function pointer from the
    interface. You then need to adjust the pointer to the right base. Then
    you call the function. It returns a void pointer which you have to
    query for another interface ... The code doesn't look like C.
     
    Malcolm McLean, Sep 13, 2010
    #14
  15. jacob navia

    Felix Palmen Guest

    As long as you can live with each object carrying its function pointers
    for the member functions (which you can, as long as you don't
    instantiate millions of objects), this is just not true. The only thing
    you really HAVE to do is passing /in/ (not out) the "this" pointer as
    (void *) and checking type compatibility. This can be perfectly hidden
    behind some macros, the code of course "looks like C".

    Regards, Felix
     
    Felix Palmen, Sep 13, 2010
    #15
  16. jacob navia

    jacob navia Guest

    Le 13/09/10 10:59, BartC a écrit :
    In principle yes, in practice no. Since there are no classes and no
    hierarchy, overloading is easy to follow. In all documentation I
    underscored that operator overloading works with types that should mimic
    the typical types used with that operators. Arithmetic operators work
    well with NUMBERS (of any kind) and should be AVOIDED with anything else
    but NUMBERS. Generalized sequential container access with the square
    brackets operator should be used for indexing ONLY.

    Some exceptions are so standard that they can be used, for instance:

    HashTable *h;
    /* ... */
    h["some key"] = foo;

    In general however, operator overloading should NOT be overused. I
    warned explicitely in my documentation against this.
     
    jacob navia, Sep 13, 2010
    #16
  17. I don't see how this is better than C++. Do you actually think most
    people writing code read the compiler documentation much? Most books
    on C++ I've read encourage very light usage of operator overloading
    (other than operator= of course), except for arithmetic types. But
    that doesn't stop newish developers from thinking that operator+ that
    they learned in school is cool, and is intuitively obvious in this
    particular case and means merge the sets or append the string or make
    the hash bigger or add to the tarfile or ...

    Above is not meant as a flame, just an observation. I agree with the
    general thrust that C++ without careful choice of features to use is
    harder to maintain than C. I don't like to have to work so hard to
    figure out which code will be executed in complicated inheritence
    schemes, for example. But I do like the standard library templates
    containers (particularly now that hash_xxx is added), cast operators,
    exceptions, simple classes (in particular for dtors/ctors), simple
    inheritance, and a few other features. But am content to use the set
    of C++ I want for those, as I don't see morphing C into C++ lite is
    helpful. Obviously all IMHO.

    -David
     
    David Resnick, Sep 13, 2010
    #17
  18. You need the base functions as well, quite often. So you'll find with
    that scheme you're squirrelling away function pointers to base methods
    within your derived classes. I'm not saying it can't be made to work,
    particularly if you ban interfaces and multiple inheritance and just
    have simple inheritance. But the approach tends not to scale.
     
    Malcolm McLean, Sep 13, 2010
    #18
  19. jacob navia

    Felix Palmen Guest

    | ((baseclass) this)->someMethod(...);
    Only if you inherit and extend functionality, which is necessary
    sometimes, but should not be common case in a good OO design. You don't
    need to save these pointers in your derived class, a static pointer in
    the implementation module of the class will do.
    Depends on what exactly you want to do with it. A lot of people consider
    multiple inheritance bad design, too. I have seen good uses of it, but
    if I want something like this, I'll probably go for C++. If I just want
    a clean object model and don't need all the stuff like MI, RAII,
    operator overloading etc, I prefer C :)

    Regards,
    Felix
     
    Felix Palmen, Sep 13, 2010
    #19
  20. <--
    You need a vtable, then you need to pass about the objects as void *s,
    then have some scheme for nesting the base class structure, and
    resolving back to the base class vtable.

    It can be done, but there's more to it than just declaring a few
    function pointers.
    -->

    years ago, I had an idea:
    allocate all memory objects with a "typename".
    this type is, in turn, linked to several different vtables (a vtable used
    internally to the GC, a vtable supplied by the types library, ...).

    then, one can do things like typecheck objects, use generic operations on
    objects ('toString()', ...).
    typically, the per-type vtables are managed by the code exporting the
    interface in question, whereas code related to the specific type is usually
    responsible for registering vtable members (or, in other cases, registering
    the vtables).

    note:
    this is essentially backwards of how JVM and similar tend to work, where the
    JVM manages each interface vtable with the class that implements it, whereas
    in my system, typically the interface manages the vtable, which would be
    looked up given the class.


    however, this system does not itself support inheritence.
    typically, inheritable objects are built on top.

    however, thus far I have not found any "good" way to do C/I OO which doesn't
    require a (typically awkward) API.

    I created a C/I system before, much more recently than the above, which had
    its design based mostly on the C/I system exposed by JNI. much like JNI, it
    proved somewhat awkward to use.

    some improvements could be made (mostly to allow a lighter-weight API), but
    would require reorganizing its internals some, which would be a PITA given
    how big and complicated the thing is, so yeah...


    so, it mostly competes with (and generally loses to) my older systems
    (systems based on the type-tag system, and typically supporting inheritence
    via daisy-chaining structs and vtables), and also to code using Prototype-OO
    (which, ironically, shares many of the same internal facilities as the C/I
    system, as the C/I system was partly just built on top of a lot of the
    substructure from the P-OO system...).


    note:
    daisy-chaining is the inverse of the typical C/I OO implementation (and also
    my main C/I system), in that it starts with the base class, and each class
    typically provides a "subclass" (or "userdata" or ...) pointer or similar,
    and so to get to each subclass, one typically has to indirect through this
    pointer.
    (to get to the final derived class type, one would have to step along until
    this pointer is NULL).

    the result is that the natural place to put most common functionality is in
    the base-class.

    potentially, this system could be combined with a typical physical extension
    C/I system (each subclass pointer effectively pointing just past its own
    physical end). note that each subclass would only define fields and vtable
    entries for its own level (rather than for all levels above it as well),
    which would mean that vtables with no overrides could be used directly from
    the parent class (no need to copy their contents into the new vtable).

    struct ObjBase_s {
    void *vtable; //both vtable and also class-info
    void *subclass;
    //fields...
    };

    it would also simplify mapping the C/I system to C, whereas mapping
    physical-extension C/I systems to C often turns nasty (like, a big mess of
    casting like in GObject, ...).

    however, one could argue that it could use more memory per-instance (due to
    the additional pointers), as well as possible access delays (since deep
    inheritence could lead to a number of indirections to access a field).


    however, the C++ ABI does not organize its classes this way, and I had used
    the C++ ABI partly for determining how to lay out class structures in
    memory, ...


    or such...
     
    BGB / cr88192, Sep 13, 2010
    #20
    1. Advertisements

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments (here). After that, you can post your question and our members will help you out.