5
\$\begingroup\$

I spent like half an hour programming up a set of functions to make the allocation / deallocate of pointers easier within a bigger project. I felt that they were satisfactory, but was wondering if there's anywhere I can improve, especially in the area of naming, usability, and performance (are there any faster versions of the allocation functions I wrapped?). I also wanted to ask; are there any places where this program could benefit from restrict? I understand the vague meaning of the keyword but have yet to come across a situation in which I can confidently say restrict was necessary.

I've already run this program through Valgrind and ensured nothing leaks. Take a look.

// Allocate.h

#include <stdio.h>
#include <memory.h>

/**
 * @brief A wrapped pointer. Includes the size of the pointer, alongside
 * the pointer itself.
 */
typedef struct
{
    /**
     * @brief An ambiguous pointer to an ambiguous location at memory.
     */
    void* inner;
    /**
     * @brief The size of the allocated pointer. Editing this will likely
     * cause problems as the interface either reads data that doesn't
     * exist, or cuts off data that @b does exist.
     */
    size_t size;
} ptr_t;

/**
 * @brief An empty wrapped pointer initializer.
 */
#define WRAPPED_POINTER_EMPTY                                             \
    (ptr_t) { NULL, 0 }

/**
 * @brief Allocate a block of @param size. The block is initialized to
 * nothing, and will be (probably) filled with garbage data.
 * @param size Size of the allocated block.
 * @return The pointer created, alongside its assigned size.
 */
ptr_t AllocateBlock(size_t size);

/**
 * @brief Much the same as @ref AllocateBlock, allocate a block of @param
 * size. This block, however, is fully initialized to the value 0.
 * @param size The size of the allocated block.
 * @return The pointer created, alongside its assigned size.
 */
ptr_t AllocateZeroedBlock(size_t size);

/**
 * @brief Free the given pointer. If this is passed a NULL pointer, the
 * fatal @enum memory_free_failure exception is raised. The pointer
 * associated with the given @struct ptr_t object is set to NULL, and the
 * size is set to 0.
 * @param ptr The pointer to be freed.
 */
void FreeBlock(ptr_t* ptr);

/**
 * @brief Copy the contents and size of a wrapped pointer to the shell of
 * another. @param dest is overwritten during this operation, but no
 * changes are made to @param src. If the two pointers are the same, if
 * either are NULL, or if the contents of @param src is NULL, nothing is
 * done.
 * @param dest The destination pointer, where things should be copied to.
 * @param src The source pointer, where things are to be copied from.
 */
void CopyBlock(ptr_t* dest, const ptr_t src);

/**
 * @brief Shrink the given pointer's usable section of memory. This is not
 * guaranteed to keep the data inside the pointer. For that behavior, check
 * out @ref ShrinkBlockSafe. If the passed-in size is greater than or equal
 * to the pointer's current size, nothing is done.
 * @param ptr The pointer to shrink.
 * @param new_size The new size of the pointer.
 */
void ShrinkBlock(ptr_t* ptr, size_t new_size);

/**
 * @brief Grow the given pointer's usable section of memory. This is not
 * guaranteed to keep the data inside the pointer. For that behavior, check
 * out @ref ExpandBlockSafe. If the passed-in size is less than or equal
 * to the pointer's current size, nothing is done.
 * @param ptr The pointer to shrink.
 * @param new_size The new size of the pointer.
 */
void ExpandBlock(ptr_t* ptr, size_t new_size);

/**
 * @brief Resize the given pointer's usable section of memory. This,
 * depending on the size passed to the function, either calls @ref
 * ShrinkBlock or @ref ExpandBlock. If the size is equal to the given
 * pointer's current size, nothing is done.
 * @param ptr The pointer to be resized.
 * @param new_size The requested new size of the pointer.
 */
void ReallocateBlock(ptr_t* ptr, size_t new_size);

/**
 * @brief Shrink the given pointer's usable section of memory. This is
 * guaranteed to keep the data inside the pointer. If the passed-in size is
 * greater than or equal to the pointer's current size, nothing is done.
 * @param ptr The pointer to shrink.
 * @param new_size The new size of the pointer.
 */
void ShrinkBlockSafe(ptr_t* ptr, size_t new_size);

/**
 * @brief Grow the given pointer's usable section of memory. This is
 * guaranteed to keep the data inside the pointer. If the passed-in size is
 * less than or equal to the pointer's current size, nothing is done.
 * @param ptr The pointer to shrink.
 * @param new_size The new size of the pointer.
 */
void ExpandBlockSafe(ptr_t* ptr, size_t new_size);

/**
 * @brief Resize the given pointer's usable section of memory. This,
 * depending on the size passed to the function, either calls @ref
 * ShrinkBlockSafe or @ref ExpandBlockSafe. If the size is equal to the
 * given pointer's current size, nothing is done.
 * @param ptr The pointer to be resized.
 * @param new_size The requested new size of the pointer.
 */
void ReallocateBlockSafe(ptr_t* ptr, size_t new_size);

/**
 * @brief Set the contents of the given pointer to the given value.
 * @param ptr The pointer to set.
 * @param contents The new contents of the pointer.
 */
static inline void SetBlockContents(ptr_t* ptr, void* contents)
{
    if (contents == NULL) return;
    memcpy(ptr->inner, contents, ptr->size);
}
// Allocate.c
#include "Allocate.h"
// Error reporting, provides "ReportError".
// Ignore it, treat it as a call to exit().
#include <Output/Error.h> 

ptr_t AllocateBlock(size_t size)
{
    ptr_t created_ptr = {NULL, size};
    created_ptr.inner = malloc(size);
    if (created_ptr.inner == NULL)
        ReportError(memory_allocation_failure, false);

    return created_ptr;
}

ptr_t AllocateZeroedBlock(size_t size)
{
    ptr_t created_ptr = {NULL, size};
    created_ptr.inner = calloc(1, size);
    if (created_ptr.inner == NULL)
        ReportError(memory_allocation_failure, false);

    return created_ptr;
}

void FreeBlock(ptr_t* ptr)
{
    if (ptr == NULL || ptr->inner == NULL)
        ReportError(memory_free_failure, false);
    free(ptr->inner);

    ptr->inner = NULL;
    ptr->size = 0;
}

void CopyBlock(ptr_t* dest, const ptr_t src)
{
    if (dest == NULL || dest->inner == src.inner) return;
    if (src.inner == NULL) return;

    if (dest->inner != NULL) FreeBlock(dest);
    *dest = AllocateBlock(src.size);
    SetBlockContents(dest, src.inner);
}

static void ChangeBlockSize(ptr_t* ptr, size_t new_size)
{
    if (ptr->inner != NULL) FreeBlock(ptr);
    *ptr = AllocateBlock(new_size);
}

void ShrinkBlock(ptr_t* ptr, size_t new_size)
{
    if (new_size >= ptr->size) return;
    ChangeBlockSize(ptr, new_size);
}

void ExpandBlock(ptr_t* ptr, size_t new_size)
{
    if (new_size <= ptr->size) return;
    ChangeBlockSize(ptr, new_size);
}

void ReallocateBlock(ptr_t* ptr, size_t new_size)
{
    if (new_size == ptr->size) return;
    if (new_size > ptr->size) ExpandBlock(ptr, new_size);
    else ShrinkBlock(ptr, new_size);
}

static void ChangeBlockSizeSafe(ptr_t* ptr, size_t new_size)
{
    ptr_t contents = AllocateBlock(new_size);
    if (ptr->inner != NULL)
    {
        SetBlockContents(&contents, ptr->inner);
        FreeBlock(ptr);
    }
    *ptr = AllocateBlock(new_size);
    SetBlockContents(ptr, contents.inner);
}

void ShrinkBlockSafe(ptr_t* ptr, size_t new_size)
{
    if (new_size >= ptr->size) return;
    ChangeBlockSizeSafe(ptr, new_size);
}

void ExpandBlockSafe(ptr_t* ptr, size_t new_size)
{
    if (new_size <= ptr->size) return;
    ChangeBlockSizeSafe(ptr, new_size);
}

void ReallocateBlockSafe(ptr_t* ptr, size_t new_size)
{
    if (new_size == ptr->size) return;
    if (new_size > ptr->size) ExpandBlockSafe(ptr, new_size);
    else ShrinkBlockSafe(ptr, new_size);
}

\$\endgroup\$
2
  • 4
    \$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. \$\endgroup\$
    – pacmaninbw
    Commented Aug 9 at 12:01
  • \$\begingroup\$ @pacmaninbw Alright, noted. \$\endgroup\$
    – Zenais
    Commented Aug 10 at 4:42

3 Answers 3

6
\$\begingroup\$

"I spent like half an hour..."

Then, I'd suggest you spend at least an hour-or-two writing code to thoroughly exercise each of these functions in all permutations and combinations of their being called.

The job's not over until the paperwork is done.
The best intentions written in comments won't save code that has bugs.

Unh!?

static inline void SetBlockContents(ptr_t* ptr, void* contents)
{
    if (contents == NULL) return;
    memcpy(ptr->inner, contents, ptr->size);
}

If the source data is char foo[12]; and the destination is ptr->size = 512, this will attempt to copy from well-beyond the end of the source array. This is UB.

Will the caller ALWAYS ensure conditions are right? If so, then this function brings nothing except bloat and ambiguity to the code.


Efficiency and a Style Recommendation

static void ChangeBlockSize(ptr_t* ptr, size_t new_size) // style 1

vs.

static void ChangeBlockSize(ptr_t *ptr, size_t new_size) // style 2

First, "style"...
There are multitudinous examples of int main( int argc, char *argv[] )
To declare a pointer to a function, one uses something like: int (*p)( int x, int y );
Note that the splat is adjacent to the variable's name, not its datatype.
Recommend always using "style 2" from the examples above.
This will get it right when you write char *p1, *p2; and variations thereof.

Now "efficiency"...
Careful examination of this function's code shows it TWICE copying the data to fulfill its function. While this may only be costly for small buffers, it may be impractical for very large buffers.

And (in the same function):

    ptr_t contents = AllocateBlock(new_size);
    if (ptr->inner != NULL)

Contradicts the notion that "allocation" is "do or die!!".
Whereas the first allocation is verified, the second allocation isn't???

And... finally...
I've not tested this, but my "valgrind spiderman sense" is tingling...
Are you 100% sure there is no memory leak when this function is used?
There are two "allocate" operations, but only one "free" operation within the function.
Please check!


Right tool for the job

(Initially posted as a comment, but added to this "answer".)
ChangeBlockSizeSafe() will ALWAYS allocate & copy contents, whereas realloc() cooperates with system memory management to minimise wasted cycles. Most every beginner seeks to "package up" or "generic-ify" their code. It's a valid exercise, but you will mature away from this stuff as your C skills grow.

Other languages offer pre-packaged functionality. Use them instead of C, if that's your objective... This is just an unsolicited opinion. Do you want to write a working "video game app" or do you want to write a light-weight, streamline "device driver"?

\$\endgroup\$
14
  • 1
    \$\begingroup\$ @Fe203 Interesting. I suppose I'll switch over to using that, then. \$\endgroup\$
    – Zenais
    Commented Aug 9 at 7:29
  • 1
    \$\begingroup\$ @Fe203 Looking now at the man pages for realloc, it solves near every problem you've referenced in your post. I appreciate your input. \$\endgroup\$
    – Zenais
    Commented Aug 9 at 7:34
  • 1
    \$\begingroup\$ Three important subtle points about realloc(): 1) store (and test) the returned pointer into a temporary variable. Don't overwrite the existing pointer variable's value. If realloc() fails, the existing allocated heap STILL exists (and the program can try something else to recover.) // 2) A grown block contains the same data, but the annexed bytes are uninitialised (even if original block comes from calloc(). // 3) realloc( NULL, someSize ); == malloc( someSize ); This is useful to start with zero allocation and grow an array as needed. Best wishes. Keep reading and experimenting! \$\endgroup\$
    – Fe2O3
    Commented Aug 9 at 7:43
  • 1
    \$\begingroup\$ Your computer (probably) has a "swap file" out there somewhere. When finite RAM is fully occupied, the OS can push blocks of data from RAM to swap space. This gives a process "virtual memory" much larger than actual RAM available. Likewise, a sophisticated program (processing many Gb of data) could "bounce", decide to free() some stale stuff, and try again to grab a block of sufficient size to keep on truckin'... Better UX than a BSOD, no? :-) (Simple case: Asked for 30. Denied. Okay, maybe ask for 10 and see if I can complete this 18hr execution with that. Never surrender!!) \$\endgroup\$
    – Fe2O3
    Commented Aug 9 at 7:58
  • 1
    \$\begingroup\$ @Fe203 That is EXTREMELY useful to know (well, I knew swap existed, but thinking of it being used in such a way is different than simply knowing it is a thing), thank you! I'll put it on the list to implement later on in the project's lifetime. \$\endgroup\$
    – Zenais
    Commented Aug 9 at 8:03
4
\$\begingroup\$

clear terminology

    ... Includes the size of the pointer, ...
     * @brief The size of the allocated pointer.

My initial read on these was we're distinguishing a NEAR from a FAR or 32-bit or 64-bit pointer. Subsequent documentation is much clearer, for example "Size of the allocated block."

Prefer to talk about the allocated block, or perhaps the [block] size "associated" with a pointer.

free'ing NULL

In FreeBlock the || ptr->inner == NULL check makes free(NULL) a fatal error. Agree or disagree with it, but free() has long promised that

If ptr is NULL, no operation is performed.

Clearly this library is free to invent its own non-POSIX public interface. The OP code's semantics are slightly surprising. If that's what is desired, fine, but documentating that "If this is passed a NULL pointer" leads to error could go a bit further to describe the ptr->inner test, as well.

braces

Consider adding a line that describes the coding style which Pull Requests are expected to conform to. We see one-line if bodies appearing without { body } braces on the following line, and even on same line as the if condition. Consider having the project's Makefile routinely invoke a C beautifier tool, to enforce the stated style goal.

\$\endgroup\$
5
  • \$\begingroup\$ Good point about the documentation change. I do have a question with the NULL fatal exception suggestion, though. Why would I not want this problem to be fatal? The way I see it, if the user is passing a NULL pointer / block to be freed, something is wrong with their code, no? \$\endgroup\$
    – Zenais
    Commented Aug 9 at 0:51
  • \$\begingroup\$ This project is governed by a .clang-format file. I figured this file was too long to include here, nor that anyone would particularly care. Should I add it to the main post? \$\endgroup\$
    – Zenais
    Commented Aug 9 at 0:53
  • \$\begingroup\$ (1.) I don't personally have a problem with "free'ing NULL is fatal!", nor would I with POSIX adopting such a position. Me? I like restrictive API's which help to call out likely buggy app behavior. I'm just saying I didn't pick that up from the OP docs, and I found the disjunct to be surprising behavior, given that we're usually cool with free(NULL). // (2.) A github URL would be the most concise way to incorporate ancillary context. Seeing either style of "abbreviated IF", lacking braces, makes me nervous about future maintainers introducing bugs. I was asking: "Please state the rules." \$\endgroup\$
    – J_H
    Commented Aug 9 at 0:57
  • \$\begingroup\$ (re. 1) Glad to see an actual explanation for this for once, rather than just free(NULL) BAD!!! Thanks for the input :) (re. 2.) Noted for future questions. I actually thought that was "bad practice" of a sort on Stackoverflow to not have everything embedded into the question's body until now. \$\endgroup\$
    – Zenais
    Commented Aug 9 at 6:32
  • \$\begingroup\$ The main reason to permit free(NULL) is to simplify cleanup code. Instead of having to individually test every pointer you might have allocated in a function, you can simply pass them all to free() and let it worry about figuring out which ones were or weren't allocated. \$\endgroup\$
    – Mark
    Commented Aug 10 at 1:39
3
\$\begingroup\$

Terminology

There's nothing safe about ShrinkBlockSafe.

If the functionality is thought to be meaningful, I'd urge you to name it according to what the function actually does.

Reallocation

I do think you are correct about providing a primitive which doesn't necessarily copy the content, and I do think realloc is wrong to guarantee the contents are copied: it's common to only use a subset of the bytes, and copying them all is inefficient.

The missing primitive -- what realloc should have been -- is a guarantee in-place reallocation.

Once you have guaranteed in-place reallocation, realloc can be implemented on top of it:

void* realloc(void* ptr, size_t new_size) {
    void* realloced = realloc_in_place(ptr, size);

    if (realloced != NULL) {
        return realloced;
    }

    size_t current_size = /* magic to get size of allocation */;

    void* new_ptr = malloc(new_size);
    memcpy(new_ptr, ptr, current_size < new_size ? currency_size : new_size);

    free(ptr);

    return new_ptr;
}

Thus, instead, you may consider the following primitives:

  • ShrinkBlockInPlace: shrinks in place, or does nothing.
  • ExpandBlockInPlace: expands in place, or does nothing.
  • ShrinkBlock: shrinks no matter what, does not copy.
  • ExpandBlock: expands no matter what, does not copy.

And just leave copying to the user, they know what need (or need not) be copied.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ You can't always leave copying to the user. What if there is adequate free space immediately before the current block? The allocator can add it to the block and shift the contents down, but the user would have to allocate a new block, for which there may not be sufficient available memory. \$\endgroup\$
    – c19
    Commented Aug 9 at 19:24
  • \$\begingroup\$ A bit silly, imho. This suggests (portable?) app source code can and will manage system heap storage better than platform specific library code. If the coder feels they can do a better job, in their application, they can implement an arena (and stay awake at night worrying they've missed an edge case or two.) \$\endgroup\$
    – Fe2O3
    Commented Aug 9 at 22:15
  • \$\begingroup\$ @c19: That's a very good point, and the first time I see it raised when discussing in-place realloc as a better primitive than copying realloc. Fortunately, I think it's not necessarily a problem: if "moving down" only occurs during block extension, then all the previous memory can remain available, and thus the user can detect the change (by comparing the result pointer to the pass in pointer) and perform the copy. \$\endgroup\$ Commented Aug 10 at 11:23
  • \$\begingroup\$ @MatthieuM. Pushing complexity up to the caller is the exact opposite of what makes for good programming. And in C it is illegal for the caller to compare the pointers because they are not pointing to the same object. \$\endgroup\$
    – c19
    Commented Aug 11 at 22:53
  • \$\begingroup\$ @c19: I will disagree heartily with the idea that empowering the caller is "pushing complexity" on them. You are correct, of course, on the pointer comparison issue. \$\endgroup\$ Commented Aug 12 at 10:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.