Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

in-place vs allocating API

Tags:

c

I need to create a library in C and I am wondering how to manage objects: returning allocated (ex: fopen, opendir) or in-place initialization (ex: GNU hcreate_r).

I understand that it is mostly a question of taste, and I'm inclined to choose the allocating API because of the convenience when doing lazy initialization (by testing if the object pointer is NULL).

However, after reading Ulrich's paper (PDF), I'm wondering if this design will cause locality of reference problems, especially if I compose objects from others:

struct opaque_composite {
    struct objectx *member1;
    struct objecty *member2;
    struct objectz *member2;
    /* ... */
};

Allocation of such an object will make a cascade of other sub-allocations. Is this a problem in practice? And are there other issues that I should be aware of?

like image 442
airman Avatar asked Nov 21 '25 11:11

airman


1 Answers

The thing to consider is whether the type of the object the function constructs is opaque. An opaque type is only forward-declared in the header file and the only thing you can do with it is having a pointer to it and passing that pointer to separately compiled API functions. FILE in the standard library is such an opaque type. For an opaque type, you have no option but have to provide an allocation and a deallocation function as the user has no other way to obtain a reference to an object of that type.

If the type is not opaque – that is, the definition of the struct is in the header file – it is more versatile to have a function that does only initialization – and, if required, another that does finalization – but no allocation and deallocation. The reason is that with this interface, the user can decide whether to put the objects on the stack…

struct widget w;
widget_init(&w, 42, "lorem ipsum");
// use widget…
widget_fini(&w);

…or on the heap.

struct widget * wp = malloc(sizeof(struct widget));
if (wp == NULL)
  exit(1);  // or do whatever
widget_init(wp, 42, "lorem ipsum");
// use widget…
widget_fini(wp);
free(wp);

If you think that this is too much typing, you – or your users themselves – can easily provide convenience functions.

inline struct widget *
new_widget(const int n, const char *const s)
{
  struct widget wp = malloc(sizeof(struct widget));
  if (wp != NULL)
    widget_init(wp, n, s);
  return wp;
}

inline void
del_widget(struct widget * wp)
{
  widget_fini(wp);
  free(wp);
}

Going the other way round is not possible.

Interfaces should always provide the essential building blocks to compose higher-level abstractions but not make legitimate uses impossible by being overly restrictive.

Of course, this leaves us with the question when to make a type opaque. A good rule of thumb – that I have first seen in the coding standards for the Linux kernel – might be to make types opaque only if there are no data members your users could meaningfully access. I think this rule should be refined a little to take into account that non-opaque types allow for “member” functions to be provided as inline versions in the header files which might be desirable from a performance point of view. On the other hand, opaque types provide better encapsulation (especially since C has no way to restrict access to a struct's members). I would also lean towards an opaque type more easily if making it not opaque would force me to #include headers into the header file of my library because they provide definitions of the types used as members in my type. (I'm okay with #includeing <stdint.h> for uint32_t. I'm a little less easy about #includeing a large header such as <unistd.h> and I'd certainly try to avoid having to #include a header from a third-party library such as <curses.h>.)

like image 113
5gon12eder Avatar answered Nov 23 '25 00:11

5gon12eder