weeding out c++ keywords from sys/sys

Christoph Mallon christoph.mallon at gmx.de
Sun Feb 15 11:19:32 PST 2009


Marcel Moolenaar schrieb:
> 
> On Feb 15, 2009, at 7:33 AM, Christoph Mallon wrote:
>> More robust error handling and less tedious resouce management 
>> directly come to mind:
>> Just look at normal C functions which allocate resources and have 
>> multiple points which can fail. They are the usual mess of if()s, goto 
>> error and lots of cleanup code. Further all this code looks pretty 
>> much the same in several modules. In C++ you write the resource 
>> handling code once (constructors/destructors) and then you cannot 
>> forget to clean up, because thanks to scoping and defined life ranges 
>> it happens automatically.
> 
> While on the surface this looks better, under the hood
> it's just the same. Worse in most likelihood, because

Of course it's the same, all these languages are Turing-complete!

> with C the programmer writes the logic that is known to
> be needed (assuming no bugs). With C++ it's the compiler

Assuming no bugs, the Ariane wouldn't have exploded, Sojourner would've 
worked perfectly from day one, Hubble wouldn't have needed a correcting 
lense, ...
The whole idea is to lower the probability of making errors. The 
resource allocation scenario I sketched earlier is one example for that: 
Lots of tedious and very similar code, which makes the process of 
writing it error prone.

> that generates code that handles all possible scenarios,
> and goes beyond what is strictly needed -- as such the
> cost tends to be higher, even when there are no errors
> or exceptions.

There is no fairy which magically makes code appear everywhere. All code 
which gets generated is clearly defined by the programmer: You write 
code for constructors and destructors. All inserted code is placed at 
the boundaries of life ranges of objects. When an object is created its 
constructor is called, when it is destroyed its destructor is called. 
The compiler keeps track where exactly the life ranges of objects begin 
and end and properly inserts the code there. The programmer determines 
the life ranges of the objects either by new/delete, aggregating objects 
in classes or creating local objects in function {} scope.

in C you write something like that:
typedef struct FOO {
   A* a;
   B* b;
   C* c;
} FOO;

FOO* Allocate_FOO()
{
   FOO* f = malloc(*f);
   if (f == NULL)
     return NULL;

   lock(some_lock);

   f->a = allocate_A();
   if (f->a == NULL)
     goto fail;

   f->b = allocate_B();
   if (f->b == NULL)
     goto fail;

   f->c = allocate_C();
   if (f->c == NULL)
     goto fail;

   unlock(some_lock);
   return f;

fail:
   if (f->b)
     deallocate_B(f->b);
   if (f->a)
     deallocate_A(f->a);
   free(f);
   unlock(some_lock);
   return NULL;
}

or worse and more error prone but seen often:

FOO* Allocate_FOO()
{
   FOO* f = malloc(*f);
   if (f == NULL)
     return NULL;

   lock(some_lock);

   f->a = allocate_A();
   if (f->a == NULL) {
      free(f);
      unlock(some_lock);
      return NULL;
   }

   f->b = allocate_B();
   if (f->b == NULL) {
     deallocate_A(f->a);
     free(f);
     /* Nobody will notice this lock leak until allocating B
      * fails some day. */
     return NULL;
   }

   f->c = allocate_C();
   if (f->c == NULL) {
     /* Whoops, allocating resource B was added later and we
      * forgot to deallocate it here. */
     deallocate_A(f->a);
     free(f);
     unlock(some_lock);
     return NULL;
   }

   unlock(some_lock);
   return f;
}


In C++ you write something like this:

class FOO {
public:
   FOO();

private:
   auto_ptr<A> a;
   auto_ptr<B> b;
   auto_ptr<C> c;
};

FOO::FOO()
{
   LockHolder l(some_lock);
   a = new A();
   b = new B();
   c = new C();
}

There is no way to have a resource leak here: No matter how the 
constructor is left (regular return or exception), the lock is released 
via the destructor of the LockHolder. Also if any of the constructors of 
A, B or C throws an exception all objects created till this point are 
properly destroyed by the destructors of the auto_ptr<>s (per language 
specification in the reverse order of declaration in the class).
I consider this a improvement: The code is more concise and writing it 
is less error prone.

Oh, let's not forget deallocating FOO:

void deallocate_FOO(FOO* f)
{
   deallocate_C(f->c);
   deallocate_B(f->b);
   deallocate_A(f->a);
}

and the C++ version:
/* No code needed, the default destructor of FOO already does The Right
  * Thing(TM), i.e. destroys all subobjects in reverse order of
  * declaration */

> I'm not saying this is a problem. All I'm saying is that
> you move responsibility from the programmer to the compiler
> and in general this comes at a (runtime_ cost. One we may
> very well accept, mind you...

Writing the code correctly once is still in the hands of the programmer. 
Copying the code where it is needed now is the job of the compiler. The 
compiler can do the more tedious parts and in this way help the programmer.

Whew, this got longer than intended. Hopefully I could point out one of 
the benefits of C++. Please bear in mind this is no anti-C campaign. I 
just want to point out that C++ provides mechanisms to help writing 
better code.


More information about the freebsd-current mailing list