Performance is orthogonal to this. To me the less you juggle with pointers and malloc/free the safer your code will be from memory leaks, misuse of unallocated memory, NULL checking issues, and easier to inspect/reason about.
Struct embedding helps with this quite a bit. And it's not possible without exposing the struct definition.
Performance gains are possible, but that's secondary.
> If one already accepts that the object should be dynamically allocated
It can still be allocated on heap, but in one continuous chunk of memory as a part of the larger struct. Hiding the struct definition would prevent this.
That's largely true, but it's often cited as a reason to avoiding malloc/free (however dubious that is).
> To me the less you juggle with pointers and malloc/free the safer your code will be from memory leaks, misuse of unallocated memory, NULL checking issues, and easier to inspect/reason about.
I don't quite agree. I've worked mostly in code bases using the pattern described earlier (an opaque pointer, a $type_create(), and a $type_destroy() function). With that pattern, I find it much easier to be certain by code inspection that a particular object or transformation is valid because as long as the pointer was allocated correctly, the object can only be modified by functions that know the type (aside from memory corruption, but that's always possible). That's usually a small set of functions that know the struct details. This fact is useful both as a library author and as the author of a library consumer. By contrast, if the struct is exposed, it's harder to identify all the places that can modify the structure's details and to be sure that invariants are maintained in all those places.
Besides that, several other failure modes are much less likely with opaque structures, including copying a structure you shouldn't, miscopying a structure that's okay to copy (e.g., off-by-one while copying), or operating on a correctly-sized block of memory that's never been initialized. You can still use unallocated memory, of course, but that's fairly easy to make safer by initializing pointers to NULL. (The analogous option for stack-initialized structs -- initializing them to zero -- is not necessarily any safer than leaving them uninitialized -- particularly if the struct contains file descriptors.)
There are tradeoffs to both approaches. To me, the ability to modify the struct in new versions of the library without breaking the ABI is a pretty major point in favor of using an opaque structure for a library's primary handle. For very simple ancillary structures that are very unlikely to change, and where the convenience of stack allocation is worthwhile, exposing the structure makes a lot of sense.
Struct embedding helps with this quite a bit. And it's not possible without exposing the struct definition.
Performance gains are possible, but that's secondary.
> If one already accepts that the object should be dynamically allocated
It can still be allocated on heap, but in one continuous chunk of memory as a part of the larger struct. Hiding the struct definition would prevent this.