CS 3x (Spring 2026) Datastructures 01a: Generic Dictionaries

In this project, we will modify our linked list map to implement a generic dictionary interface; then begin implementing a hash map using this interface.

Setup

Register for the project using the registration tool and clone the repository as explained in the setup instructions.

You MUST work solo on this project! You may not collaborate at all with anyone else! Please use the SSH link found at the picture after clicking the link in the registration email:

img

Introduction

In webserver01, we implemented a dictionary that uses a linked list to support retrieval of values given a key. However, in the future, we may want our dictionary to be sorted, or to use non-char * strings and values, or to use a different backing data structure. In these cases, we want to be able to interact with our dictionary without being tied to the specific methods defined by our ll_map_t implementation. To resolve this, we will perform generification on our dictionary, where we will define an interface that all dictionaries must abide by, and then allow the individual implementations of the dictionary to add instance variables and other public methods on top of this interface. We’ve seen this idea in CS 2 already: whenever we needed to access a dictionary in Java, we could use the Map interface if we didn’t care how the dictionary was implemented. One of our goals for this project is to port this idea over to C. Here, our public interface for the dictionary will contain the following methods that we believe all dictionaries should perform:

Outside of requiring these methods, we don’t constrain down the dictionaries any further: their keys and values can be any type we want, they can support any additional methods they want, and they can store any hidden data that we want. As long as our public methods are all supported, any type of dictionary will fit into our interface.

Interfaces, in C

In an interface, we have some function headers that state what an implementation can do. For example, a dictionary can have key-value pairs added (dict_put) or retrieved (dict_get), among other things. One of the strengths of interfaces is that a client does not need to choose a specific implementation. If we write a function that takes dict_t * as an argument, a client can pass in any kind of dictionary! However, different implementations of an interface will need to store different data, as well as different specific functions (for example, a linked list has no need for a hash function). Additionally, each instance of a generic dictionary may have different freeing needs. So how do we accomplish this, while still having a unified interface in one type? We can extend our use of void * and function pointers, with a little bit of casting magic.

First, note the dict_t struct in include/dict.h. Here, we declare all the functions that a dictionary must implement, and require that an instance of the struct provide function pointers to implementations. We also have two other fields: map_impl (a generic pointer to instance-specific data), and funcs (an array of instance-specific function pointers). We declare an instance of this struct in library/ll.c, as _ll_base. This instance is static. In C, the static keyword means that this global function or variable is “seen” only in the file in which it’s declared, similar to “private” in Java.

Still in ll.c, the ll_init function is our linked list constructor. It takes one argument, a ll_funcs_t struct that contains functions specific to this particular linked list instance (see include/ll.h). We call dict_init with the following arguments:

So what’s happening with funcs and the sizeof here – we’re casting it to a totally different type, how is this valid? Looking back at the ll_funcs_t definition, note the “__attribute__((packed))”. This is an “attribute” that guarantees that each field of the struct is directly adjacent in memory. Since every field is a (function) pointer, we know that the packed memory layout of ll_funcs_t is equivalent to an array of function pointers. Thus, this cast is valid! Because the pointer doesn’t preserve the struct size, we also pass the number of bytes in the array as the last argument. (Why might struct fields not be adjacent? The compiler might insert padding bytes for “alignment”, which is out of scope for this course.)

Looking inside dict_init, a few things happen:

Generifying ll_map_t

In order to get ourselves comfortable with the new dictionary interface, we’re going to convert our linked list dictionary from webserver01 to use our new interface. However, as we make this change, there are a couple of things we should keep in mind:

With these changes in mind, we are ready to implement our linked list using the new dictionary interface:

Fixed-Size Hashmap

We will now use our dictionary interface to support another type of dictionary: hash tables! In this project, we will implement a linear-probing hash table, just like the one we discussed in lecture. As with our linked list implementation, we will need to support each of six methods implemented in the public interface. Here are some things to keep in mind as you implement the hash table:

Other than these constraints, we will leave the design of the hash table up to you. As long as you have implemented a linear-probing hash table that correctly supports the public interface we’ve provided, you may design the internal structure and helper functions of the hash table however you’d like.

Once you’ve implemented both the linked list and hash table dictionaries, run make test to run all the tests.