(e-mail address removed):
This again assumes RAII is only about releasing memory. If this was the
case, one could just add a Boehm garbage collector to the program and
forget about all those issues. In reality, RAII in C++ is equivalently
important for mutex unlocking, file handle closing, ending the sandclock
mode of the mouse cursor, etc, etc.
Originally RAII was designed to handle memory cleanup in the presence
of exceptions, as it's pretty much a necessity to write exception safe
code in C++. I would say that when people saw how good RAII was in
memory cleanup that it was applied to other resource cleanup as well.
Hence the phrase "limited sense", as C does not provide language
support of anything resembling destructors.
For stack allocations there is the alloca() function, and also C99 VLA-s,
no need to reinvent the wheel. Or is this something different you are
talking about?
A general problem with using variable amounts of stack memory is that the
total amount of the stack space is quite small and implementation-
dependent and a stack overflow yields UB without any standard detection
or interception means. So writing a robust program with variable-size
stack allocations is pretty hard.
The difference is that this mechanism is a "fixed" stack allocation.
One issue is that alloca is not standard, and C99 VLAs do not apply to
C90 environments, which limit the scope of environments they can be
applied. While the 'region' technique has the same recursion issues
that alloca and C99 VLAs have, the stack size reserved to an algorithm
that needs to allocate memory is fixed. That means that passing big
input doesn't result in a big alloca or VLA resize that blows up the
stack.
Here's an excerpt from my region allocator.
\code
struct c_region
{
/*! \brief The start of the memory buffer. */
unsigned char* start;
/*! \brief The end of the memory buffer. */
unsigned char* end;
/*! \brief The next available address to allocate from in the
region. */
unsigned char* current;
};
void c_region_initialize( struct c_region* region, void* buffer,
size_t size )
{
c_return_if_fail( region != NULL );
c_return_if_fail( buffer != NULL );
region->start = buffer;
region->end = (unsigned char*)buffer + size;
region->current = buffer;
C_REGION_INVARIANT( region );
}
void* c_region_allocate( struct c_region* region,
size_t size,
size_t alignment )
{
unsigned char* aligned_p;
c_return_value_if_fail( region != NULL, NULL );
C_REGION_INVARIANT( region );
size = size == 0 ? 1 : size;
alignment = alignment == 0 ?
ALIGN_MAX :
c_round_up_to_power_of_two( alignment );
c_assert( c_is_power_of_two( alignment ) );
aligned_p = c_align_up( region->current, alignment );
c_assert( c_is_aligned_ptr( aligned_p, alignment ) );
/* To determine whether the region has enough space, one must
factor in the alignment padding as well as the object size. */
if ( aligned_p + size > region->end ) {
return NULL;
}
region->current = aligned_p + size;
C_REGION_INVARIANT( region );
return aligned_p;
}
\endcode
\code
int c_levenshtein( const char* s1, const char* s2 )
{
int distance;
int m, n;
int* proximity_matrix;
struct c_region pm_region;
unsigned char pm_workspace[256];
bool c_malloc_used = false;
c_return_value_if_fail( s1 != NULL, -1 );
c_return_value_if_fail( s2 != NULL, -1 );
m = strlen( s1 );
n = strlen( s2 );
/* If one of the strings is empty "", the edit distance is equal
to the length of the non-empty string. */
if ( m == 0 || n == 0 ) {
return m + n;
}
c_region_initialize( &pm_region, pm_workspace, sizeof
pm_workspace );
proximity_matrix = c_region_allocate( &pm_region,
sizeof (int) * (m+1) * (n+1),
alignof (int) );
if ( proximity_matrix == NULL )
{
proximity_matrix = c_malloc( sizeof (int) * (m+1) * (n+1) );
c_malloc_used = true;
}
if ( proximity_matrix )
{
++m;
++n;
gc_compute_levenshtein_matrix( s1, s2, &m, &n, proximity_matrix );
distance = proximity_matrix[m*n-1];
if ( c_malloc_used ) {
c_free( proximity_matrix );
}
}
else {
distance = -1;
}
return distance;
}
\endcode
The key point to take away from this example is that the size of the
stack used is independent of the size of s1 and s2. While alloca and
VLAs are convenient, they lend themself to a programming style where
it's easy to blow the stack in the presence of a big string. In my
case, I structure the stack allocation so that if it exceeds the
maximum amount I reserved for it (the size of pm_workspace), I return
*NULL*. Adjusting the amount of stack space reserved for
'c_levenshtein' is done by changing the size of 'pm_workspace',
eliminating any manual code to delineate allocations between an alloca
or VLA and malloc; the logic is built into the c_region interface.
One can also leverage a static buffer in the same manner, although one
needs to manually reset the c_region after finished with the workspace
memory.
I hope that adequately explains the benefit of going to the trouble of
using a region allocator.
Best regards,
John D.