What Copying My Own Working Code Taught Me About Copying Code
My singly linked list worked, fully tested. Adapting it into a doubly linked one should have been the easy fifteen minutes of the session. I put three bugs into it instead, all from the same bad habit.
My singly linked list worked — insert, delete, traverse, all tested. Turning it into a doubly linked version felt like it should've been the easy fifteen minutes of the session. Instead I put three bugs into it, and looking back, all three came from the same thing: I trusted a pattern that already worked instead of actually rechecking it for the new shape.
The setup
The original was just struct node { int data; struct node *next; }; with the usual head-pointer functions, all of it working. The next exercise was to add a prev pointer and turn it into a doubly linked list, struct dnode, and in my head I filed that under "small delta on something that already works." That exact assumption is what got me three separate times in the same sitting.
What went wrong
First bug was in the struct itself:
struct dnode {
int data;
struct node *next;
struct node *prev;
};I renamed the tag to dnode and stopped there. The two pointer fields were still typed as struct node *, which doesn't even exist as a meaningful type here anymore — I'd copied the field list from the old version and only really read the one part I was changing, the tag at the top.
Second bug showed up in insert_head:
void insert_head(struct dnode **head, int val) {
struct dnode *n = malloc(sizeof(*n));
n->data = value;
...
}The parameter is val. The body assigns from value, which isn't declared anywhere in this function. That's copy-paste residue from an earlier draft where I'd actually called the parameter value — when I shortened it to val I only updated the signature, not the line inside the body still using the old name.
Third bug was the one that actually ate time. My dnode struct doesn't have a tail pointer, only head, so finding the last node still means walking forward from the front even though every node along the way has a working prev. To find the node just before the tail, I needed a variable — called it saved — that had to still be around after the loop finished. First attempt, I declared it inside the while body:
while (cur->next != NULL) {
struct dnode *saved = cur;
cur = cur->next;
}
/* saved: doesn't exist here */That's a fresh saved created and destroyed every single iteration, so by the time the loop exits there's nothing left to read, and the compiler told me so. Second attempt I moved it, but into an if nested inside the same loop, same problem one level deeper. Took two wrong tries before it landed on declaring it outside the loop entirely, at the same brace level the loop itself sits at.
The fix
struct dnode {
int data;
struct dnode *next;
struct dnode *prev;
};
void insert_head(struct dnode **head, int val) {
struct dnode *n = malloc(sizeof(*n));
n->data = val;
n->prev = NULL;
n->next = *head;
if (*head) (*head)->prev = n;
*head = n;
}
struct dnode *saved = NULL;
struct dnode *cur = *head;
while (cur->next != NULL) {
saved = cur;
cur = cur->next;
}
/* saved now correctly holds the node before the tail */Every field type actually traces back to dnode now, every name inside a function matches its own signature, and saved gets declared once, above the loop, so it's still around after the loop ends instead of vanishing along with the last iteration's stack frame.
I think adapting a pattern that already works is its own way of messing up, kind of separate from writing something wrong from scratch — none of these three were really logic bugs, more like consistency slips, places where a name or a type quietly didn't get updated along with everything else. What I want to actually do differently: when I'm adapting old code instead of writing new code, go back and recheck every type and every parameter name against the new context, instead of assuming something's fine just because it looks like the old version. Whether I'll actually remember to do that under time pressure, I'm not sure — this whole session is proof I don't do it automatically. And specifically for the third bug: a variable that needs to survive a loop has to be declared outside it, at the same brace level, not nested inside anything the loop's body has. Hoping that one's simple enough to actually stick this time.
Notes from readers
Comments — via GitHub