O. Th. B.
O. Th. B.

Reputation: 1353

Regarding typedefs of 1-element arrays in C

Sometimes, in C, you do this:

typedef struct foo {
   unsigned int some_data;
} foo; /* btw, foo_t is discouraged */

To use this new type in an OO-sort-of-way, you might have alloc/free pairs like these:

foo *foo_alloc(/* various "constructor" params */);
void foo_free(foo *bar);

Or, alternatively init/clear pairs (perhaps returning error-codes):

int foo_init(foo *bar, /* and various "constructor" params */);
int foo_clear(foo *bar);

I have seen the following idiom used, in particular in the MPFR library:

struct foo {
   unsigned int some_data;
};
typedef struct foo foo[1]; /* <- notice, 1-element array */
typedef struct foo *foo_ptr; /* let's create a ptr-type */

The alloc/free and init/clear pairs now read:

foo_ptr foo_alloc(/* various "constructor" params */);
void foo_free(foo_ptr bar);
int foo_init(foo_ptr bar, /* and various "constructor" params */);
int foo_clear(foo_ptr bar);

Now you can use it all like this (for instance, the init/clear pairs):

int main()
{  
   foo bar; /* constructed but NOT initialized yet */
   foo_init(bar); /* initialize bar object, alloc stuff on heap, etc. */
   /* use bar */
   foo_clear(bar); /* clear bar object, free stuff on heap, etc. */
}

Remarks: The init/clear pair seems to allow for a more generic way of initializing and clearing out objects. Compared to the alloc/free pair, the init/clear pair requires that a "shallow" object has already been constructed. The "deep" construction is done using init.

Question: Are there any non-obvious pitfalls of the 1-element array "type-idiom"?

Upvotes: 13

Views: 592

Answers (2)

Keith Thompson
Keith Thompson

Reputation: 263227

This is very clever (but see below).

It encourages the misleading idea that C function arguments can be passed by reference.

If I see this in a C program:

foo bar;
foo_init(bar);

I know that the call to foo_init does not modify the value of bar. I also know that the code passes the value of bar to a function when it hasn't initialized it, which is very probably undefined behavior.

Unless I happen to know that foo is a typedef for an array type. Then I suddenly realize that foo_init(bar) is not passing the value of bar, but the address of its first element. And now every time I see something that refers to type foo, or to an object of type foo, I have to think about how foo was defined as a typedef for a single-element array before I can understand the code.

It is an attempt to make C look like something it's not, not unlike things like:

#define BEGIN {
#define END }

and so forth. And it doesn't result in code that's easier to understand because it uses features that C doesn't support directly. It results in code that's harder to understand (especially to readers who know C well), because you have to understand both the customized declarations and the underlying C semantics that make the whole thing work.

If you want to pass pointers around, just pass pointers around, and do it explicitly. See, for example, the use of FILE* in the various standard functions defined in <stdio.h>. There is no attempt to hide pointers behind macros or typedefs, and C programmers have been using that interface for decades.

If you want to write code that looks like it's passing arguments by reference, define some function-like macros, and give them all-caps names so knowledgeable readers will know that something odd is going on.

I said above that this is "clever". I'm reminded of something I did when I was first learning the C language:

#define EVER ;;

which let me write an infinite loop as:

for (EVER) {
    /* ... */
}

At the time, I thought it was clever.

I still think it's clever. I just no longer think that's a good thing.

Upvotes: 16

Justin Meiners
Justin Meiners

Reputation: 11113

The only advantage to this method is nicer looking code and easier typing. It allows the user to create the struct on the stack without dynamic allocation like so:

foo bar;

However, the structure can still be passed to functions that require a pointer type, without requiring the user to convert to a pointer with &bar every time.

foo_init(bar);

Without the 1 element array, it would require either an alloc function as you mentioned, or constant & usage.

foo_init(&bar);

The only pitfall I can think of is the normal concerns associated with direct stack allocation. If this in a library used by other code, updates to the struct may break client code in the future, which would not happen when using an alloc free pair.

Upvotes: 3

Related Questions