Episode 10 – Types (Part 2: Compatible & Composite Types)
First of all I want to apologise for the complexity of last episode. It was really hard for me as well to put together, and the topic, when faced for the first time, is a true punch in the nose. We are all used to types and categories in our everyday life, we may still even have a few memories from our philosophy classes on Immanuel Kant’s Critique of the Pure Reason, where categories of classification made their first appearance. One of the most interesting aspects for me has been the fact that “nothingness” has indeed its own type in programming because, well, “nothing” cannot exist in a numerical universe. Take mathematics: zero represents the absence of value, and yet it is not a blank spot in our minds or on our papers, it is a vertical ellipse of ink, something tangible and real.
But let us not indulge further into Romanticism, and tackle the next chunk of our journey into C types: let me introduce you to compatible and composite types.
Let’s get started.
The first thing we need to get out of the door is the concept of translation unit. Once more it has a scary name but, in short, it is just a file where your code is written. For example, the
main.c file in your Xcode project is a single translation unit. If you create a new file to add functionality to your program, for example
instruments.c, that file is another translation unit. Now that the basic concept is clear, let’s get a bit deeper. In Episode 5, we looked at the different phases of translations, the process during which your code becomes an executable program. At a certain point during such process, your source file is passed through the preprocessor, meaning that its header files (those included with the
#include directive) are expanded and literally included, sections of code which are optional due to
#ifndef tokens (we will get there!) are included (or not), and macros1 have been expanded.
Now for the important part: in a C program, declarations (i.e., fragments of code ending with
;) referring to the same object or function in separate translation units do not have to use the same type, rather sufficiently similar types, known as compatible types. This is possibly an unnecessary complication, since languages such as C++ do not use this separation: two types are either the same or they are not. Let us specify that when we say “referring to the same object”, we often mean different instances of the same structure, not necessarily the same integer variable accessed from two different files. Being compatible has the obvious advantage of allowing you to manipulate different objects together.
Two types, for generic purposes we will use the letters
U, both uppercase, are named compatible if any of the following conditions apply:
- they are the same exact type
- they are both pointer types, and they point to types which are already compatible
- they are array types and their elements are compatible (for example, two arrays of integers to store the frequency of some pitches). If they are constant in size, they must have the same size to be compatible.
- they are both structures, unions, or enumerations, and they have the same tag (for example, they are both
struct). Moreover, if their declaration is already complete, they need to have the same amount of members, all of which declared with compatible types, and have matching names. In short, if they are the same, it is easier for you!
- If the two types are enumerations, their raw value of the members (the integer hidden inside each member) must be the same.
- If the two types are structures, the corresponding members must be declared in the same order.
- they are function types and their return types are compatible, for example both functions return integers. If they have parameters, the number of parameters must be the same.
To make an example, the character type
char is incompatible with either
signed char and
unsigned char. Should you be using two incompatible types, the compiler would throw an error for “undefined behaviour”. Once we delve into practical examples we will have a lot of time to dedicate to understanding how this work, but for now, just try to familiarise with the concept of two compatible types. If you feel it too nebulous, never mind, skip it for now, as said, we will come back to it.
If you found the previous section complex and felt like skipping it, you may skip this as well, but I believe it worth a read. A composite type is a new type that is constructed from two types which are already deemed compatible. Beyond being compatible with both original types, it must satisfy a few conditions.
- If types
Xare arrays, composite type
Wmust pay attention to the size of the original arrays, as anything that is undefined is potentially dangerous. In short, if one array has a known constant size, the composite type is an array of that size. There are some arrays defined as of variable length, VLA, that, if their size is not clear enough, causes undefined behaviour in the compiler. It is better, to avoid issues, to either give a precise size to the array, or to leave it unknown, so that the composite array will also have an unknown size.
- If one of
Xis a function type with a parameter list, then the composite type will inherit such parameter list.
- If both
Xare function types with a parameter list, the type of each parameter in the composite parameter type list will be a composite type of the corresponding parameters (yes, you can breathe now!).
Once more, this is just a theory lesson, where we need to familiarise with the terminology. Without a practical example, and there are plenty of higher priority ones to be looked at before, it is normal that all this will make little to no sense. This is why I am not adding any musical connection in this lesson, as this is also very hard for me to grasp, and almost entirely new. Used to the cleaner Swift syntax, getting back to C is easy as long as we stay away from pointers and parentheses. For example, the documentation for composite types would propose something like this:
int f(int (*)(), double (*));
I am unfortunately unsure of how to properly read this, but let’s try: this is a function (not because of the
f identifier, which is the only easy thing in this line), with a return type of
int. This function accepts two parameters, another function which is also a pointer and returns an integer, and a pointer array of doubles capable of holding three elements. I hope you will agree that this stuff is really hard and that it is normal if our brain feels like melting when facing it.
I think we can call it a day for this episode, and, finally, in the next one, we will tackle some practical examples of each of the types we have encountered so far.
Thank you for your patience, and I hope you are still following me after these last two very complex episodes.
Thank you for reading today’s article.
If you have any question or suggestion, please leave a comment below or contact me using the dedicated contact form. Assuming you do not already do so, please subscribe to my newsletter on Gumroad, to receive exclusive discounts and free products.
I hope you found this article helpful, if you did, please like it and share it with your friends and peers. Don’t forget to follow me on this blog and to let me know what you think.
If you are interested in my music engraving services and publications don’t forget to visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).
Thank you so much for reading!
Until the next one, this is Michele, the Music Designer.
- Macros are another kind of shortcut that let us define a value that we want to use in our program many times. It is similar in functionality to a file scope variable, and it is introduced with the
#define notes = 12;↩