Opaque objects on the stack / in structs

cobject

I have a collection of opaque objects and related functions API. Such objects are typically created on the heap, but that involves a non-trivial amount of overhead. And when aggregate together, that adds layers upon layers of extra pointers and indirection.

I would like to be able to compose multiples of those opaque types in structures or as stack objects in order to eliminate the heap memory overhead.

I do know the sizes and alignments of those opaque types, so what would be the optimal way to present that information to the compiler?

I am thinking fixed size byte arrays with explicit alignment. Just wanted to check for a second opinion on whether this is the way to go, and for any potential pitfalls.

Edit: hopefully this clarifies my intent:

struct aggregateHeap {
    OpaqueType1 * op1;
    OpaqueType2 * op2;
    OpaqueType3 * op3;
}; // 3 extra pointers + heap allocation + additional indirection level overhead

struct aggregateStack {
    AlignedBlob(OpaqueType1Size, OpaqueType1Align) op1;
    AlignedBlob(OpaqueType2Size, OpaqueType2Align) op2;
    AlignedBlob(OpaqueType3Size, OpaqueType3Align) op3;
}; // no overhead

void fooHeap() {
  OpaqueType1 * op1 = createOpaqueType1();
  OpaqueType2 * op2 = createOpaqueType2();
  OpaqueType3 * op3 = createOpaqueType3();  
  // use opaque objects with api
} // heap allocation in critical section

void fooStack() {
  AlignedBlob(OpaqueType1Size, OpaqueType1Align) op1;
  AlignedBlob(OpaqueType2Size, OpaqueType2Align) op2;
  AlignedBlob(OpaqueType3Size, OpaqueType3Align) op3;  
  createOpaqueType1(op1);
  createOpaqueType2(op2);
  createOpaqueType3(op3);
  // use opaque objects with api
} // no allocation overhead

void fooAggregateStack() {
  struct aggregateStack agg;
  InitAggregate(&agg);
  // use agg
} // no overhead, much cleaner

Best Answer

The typical way to create allocatable opaque types is to give up on the opaqueness in the strictest sense, and to declare access to internals as UB. For example, the POSIX pid_t type happens to be a typedef int pid_t in practice.

But here it is important to understand your actual goals.

  • Are you merely concerned about malloc() efficiency?

    If so, create a function that uses an arena allocator. You can't beat the actual stack, but you can get pretty close.

  • Do you want to avoid pointer indirection?

    If so, the type may not have to be totally opaque. In case of C++, you can also declare struct members to be private.

    Also, pointers are not necessarily evil and inefficient. They are most problematic when they make CPU caching impossible, i.e. when you have performance-sensitive code with more data than fits into the CPU caches, and use unpredictable memory access patterns. But even the standard glibc allocator provides good memory locality in most cases, at least for applications that are not very long-running.

  • Are you using plain C and want to avoid pointer indirection and want to keep your types as opaque as possible?

    Strictly speaking, this is not possible. But you can emulate it. Such emulation only makes sense if:

    • you really cannot use C++ (where you'd just declare struct contents private instead);
    • you are writing a dynamically linked library that also needs a clear encapsulation boundary; and
    • you're committed to manually ensuring ABI compatibility with your true struct layout.

    Then, I would recommend to create a struct that has a single char[N] array member of sufficient size, and use appropriate annotations to ensure correct alignment of the struct. You can then reinterpret-cast this struct to your true struct type inside your functions. I think this is technically UB, but safe in practice – everyone does this.

An example of an opaque-ish type is the pthreads mutex type (pthread_mutex_t). In most implementations, it is defined as a struct that contains some metadata + a char array that will be used for internal storage. It can be initialized with the pthread_mutex_init(pthread_mutex_t*, args) function, or assigned directly from the macro PTHREAD_MUTEX_INITIALIZER to get default settings. Of course, the contents of this macro need to know the struct's true internals, but ideally it can just be zero-initialized.

Related Topic