Article summary
I love C’s structs. There’s a lot of weirdness in C but, for the most part, structs are predictable, useful, and easy to understand.
Structs, for those not familiar with C, are collections of data. An example of a struct is a point on a Cartesian plane:
struct point {
int x;
int y;
};
Normally, structs are used to associate two or more pieces of data. As the title suggests, I’m going to demonstrate why you might want to use a struct with a single element.
Preserve Array Type Information
Unlike structs, C’s arrays are bothersome, have many surprising corner cases, and are hard for beginners to differentiate from pointers (which are already hard enough to understand on their own). What I find most bothersome about C’s arrays is that they cast aside their size information as soon as they are referenced across a function or module boundary.
Let’s go through a few examples using C’s sizeof operator. First, we’ll cover the unsurprising behaviors of arrays. In this example, we allocate the the array and reference it by the name under which it was declared.
void direct_reference() {
uint8_t example_array[10];
printf("%lu\n", sizeof(example_array));
}
int main() {
direct_reference();
return 0;
}
This example, when run, prints the value ‘10’ as we’d expect. If we instead replace the function direct_reference
with this new function called indirect_reference
, we see different behavior.
void indirect_reference(uint8_t referenced_array[10]) {
printf("%lu\n", sizeof(referenced_array));
}
int main() {
uint8_t example_array[10];
indirect_reference(example_array);
return 0;
}
On my 64-bit machine, this example prints the value ‘8’, which happens to be the size of a pointer on my system. Note that sizeof
treats referenced_array
as a pointer even though we’ve told it explicitly what type referenced_array
is.
Fortunately, we can use a single-member struct to preserve this size information across reference boundaries! Here’s an example.
struct array_wrapper {
uint8_t array[10];
};
void indirect_reference(struct array_wrapper * a) {
printf("%lu\n", sizeof(a->array));
}
int main() {
struct array_wrapper ar;
indirect_reference(&ar);
return 0;
}
In this example, we define a struct with a single field — the array. Interestingly enough, if we pass around a reference to the struct and perform a sizeof on the array inside of the struct, the array’s size is preserved — even across reference boundaries. The struct preserves all the sizing information of its members. Running this code prints the value ‘10’, as we’d expect.
This behavior is also consistent when using external references (using ‘extern’).
EDIT: User ‘nooneofnote’ on reddit.com points out that the same size-preserving effect can be achieved by using typedef
instead of a wrapping struct.
Prevent Unwanted Type Coercion
C provides a mechanism by which we can rename types. The typedef
keyword gives us a way to make types like count_t
that are really int32_t
underneath. This is nice for semantics, but it doesn’t give any additional compile time safety.
There’s nothing to prevent you from, say, defining a type named seconds
and another type named milliseconds
and then accidentally adding them together! C will happily look behind the typedef
in both cases, see that both types are really integers, and add them together! This, of course, likely yields a meaningless result. If you add 5 milliseconds to 10 seconds, you’ll either end up with a value of 15 seconds or milliseconds. This is clearly not the desired behavior.
As you may have guessed, we can use another single-member struct to give us that additional compile-time safety that typedef
alone doesn’t provide! In the following example, we can see the scenario where milliseconds and seconds are being added erroneously.
typedef uint32_t seconds_t;
typedef uint32_t milliseconds_t;
int main() {
seconds_t x = 10;
milliseconds_t y = 20;
// oops! seconds_t result = x + y;
printf("seconds: %u\n", result);
}
We can provide a little more safety here by doing two things. First, instead of using typedef
, we can wrap the values in a struct! Secondly, we can define a function that performs the second addition for us. Here’s our extended example.
struct seconds { uint32_t val; };
struct milliseconds { uint32_t val; };
struct seconds add_seconds(struct seconds a, struct seconds b) {
return (struct seconds) { a.val + b.val };
}
int main() {
struct seconds x = { 10 };
struct milliseconds y = { 20 };
// oops!
struct seconds result = add_seconds(x, y);
printf("seconds: %u\n", result.val);
}
In this example, we pass the integers wrapped in the structures by value to a function responsible for adding them together. As you may be able to tell, this example won’t compile! My compiler gives me the following result when I try to build this example:
error: passing 'struct milliseconds' to parameter of incompatible type 'struct seconds'
struct seconds result = add_seconds(x, y);
What we’ve done is create a circumstance where the compiler can be more proactive about finding our mistakes. By carrying along the additional struct types, we help reduce the number of mistakes we make when mixing different types that the compiler would normally consider to be interoperable.
I love this use of c structs so much.
Great read. Thanks for the insight…
The section “Prevent Unwanted Type Coercion” is really more to do with type equivalence algorithms used in C. C uses “name equivalence” criterion for structs and union types and “structural equivalence” for everything else. In the example, the variable x has type “struct seconds” and the variable y has type “struct milliseconds” and therefore not considered type compatible resulting in a compile-time error.
Good article.
Puis-je prendre deux ou trois lignes sur mon blog ?
sir can you tell me,c language how much important for a beginner ?