Normally, I wouldn't go past three layers (two -> operators). I agree that too many of these chained together makes it very hard to read. I am familiar with this concept in Ruby as well, where too many chained-together methods can be a real nuisance.
I never go beyond one level of ->
[and I've been writing C for 35 years]. There is almost always a way to avoid the multiple levels.
In your example, because head
is [AFAICT] global, it goes faster if head
can be put into a function scope variable for the duration (e.g. local = head
at fnc start and head = local
at end) when aliasing considerations are at play (i.e.) head
must be fetched/stored to memory on each iteration because the compiler can't discount the fact that head
may be updated in some way it can't see or anticipate (more on this below).
Also, with the more split out code, it's easier to add conditionally compiled in debug statements and assert
checks. And, perhaps, more importantly, it's possible to add blow-by-blow comments that show intent [as you did]. The more complex the "equation", the harder it is to do this.
typedef struct Data {
int x;
int y;
} Data;
typedef struct DataNode {
struct DataNode *next;
struct Data *data;
} DataNode;
DataNode *head;
#ifdef DEBUG
#define dbgprt(_fmt...) printf(_fmt)
#else
#define dbgprt(_fmt...) /**/
#endif
DataNode *
findPrevMatching3(int x, int y)
{
DataNode *hd = head;
DataNode *next = hd->next;
Data *data = next->data;
int thisX;
int thisY;
while (1) {
thisX = data->x;
thisY = data->y;
dbgprt("findPrevMatching3: thisX=%d thisY=%d\n",thisX,thisY);
if (thisX != x)
break;
if (thisY != y)
break;
// Assign head->next to head
hd = hd->next;
// Assign each local variable, using the new head
next = hd->next;
data = next->data;
}
head = hd;
return hd;
}
Without a -DDEBUG
, one might think that the above is less efficient [because of the two separate if/break
sequences] than the original:
while (!(thisX == x && thisY == y))
but, once again, the optimizer will generate similar code (i.e. 7 insts / loop) It produces a function that is 0x33 bytes in size [on x86
with -O2
].
Here is a slightly streamlined version. To me, it is the simplest and easiest to understand version yet. It is also 7 insts / loop, but the code size is reduced to 0x2B bytes in size. So, ironically, it's also the most compact and fastest version.
typedef struct Data {
int x;
int y;
} Data;
typedef struct DataNode {
struct DataNode *next;
struct Data *data;
} DataNode;
DataNode *head;
#ifdef DEBUG
#define dbgprt(_fmt...) printf(_fmt)
#else
#define dbgprt(_fmt...) /**/
#endif
DataNode *
findPrevMatching3(int x, int y)
{
DataNode *hd = head;
DataNode *next;
Data *data;
int thisX;
int thisY;
while (1) {
next = hd->next;
data = next->data;
thisX = data->x;
thisY = data->y;
dbgprt("findPrevMatching3: thisX=%d thisY=%d\n",thisX,thisY);
if (thisX != x)
break;
if (thisY != y)
break;
// Assign head->next to head
hd = hd->next;
// Assign each local variable, using the new head
}
head = hd;
return hd;
}
For more on what I mean by "aliasing considerations", see my answer: Is accessing statically or dynamically allocated memory faster?
Historical note: In "Elements of Programming Style", by Brian Kernigan [co-creator of C] and P.J. Plauger, they say: "Make it right before you make it faster"
Here, we've shown that when you make it right, you also make it faster.