4
\$\begingroup\$

I wrote an implementation of a HashTable that uses bucket lists to store the key-value pairs implemented with a linked list.

Here's the header:

//HashTable.h
#ifndef HASHTABLE_H
#define HASHTABLE_H
#include "../../List/include/List.h"

/**
 * Implementation of a Hashtable based on bucket lists made with Linkedlist
 */

template<typename K, typename V> class HashList;

template<typename K, typename V>
class HashPair
{
    private:

        friend class HashList<K,V>;
        K key;
        V value;

    public:

        HashPair();
        // Default constructor

        HashPair(const K key,const V value);
        // constructs a new hash pair given a key and a value

        K getKey() const;
        // returns the key

        void setKey(const K key);
        // sets the key

        V getValue() const;
        // returns the value

        void setValue(const V v);
        // sets the value

};

template<typename K, typename V>
class HashList
{
    private:

        List<HashPair<K,V>> l;

    public:

        List_iterator<HashPair<K,V>> find(const K key) const;
        // Returns an HashPair given a key if present, null if absent

        void insert(const K key,const V value) const;
        // Inserts a key-value pair in the HashList

        V lookup(const K key) const;
        // Returns a reference to an HashPair value given a key if present; null otherwise

        void remove(const K key) const;
        // Removes an element given a key

        bool empty() const;
        // Returns true if the list is empty, false otherwise

        List_iterator<HashPair<K,V>> begin() const;

        List_iterator<HashPair<K,V>> end() const;

        bool finished(List_iterator<HashPair<K,V>> const p) const;

};

template<typename K, typename V>
class HashTable;

template<typename K, typename V>
class hash_iterator;

template<typename K, typename V>
bool operator ==(const hash_iterator<K,V> it, const hash_iterator<K,V> it2);

template<typename K, typename V>
bool operator !=(const hash_iterator<K,V> it, const hash_iterator<K,V> it2);

template<typename K, typename V>
class hash_iterator
{
    private:

        HashTable<K,V>* baseTable;
        int i;
        List_iterator<HashPair<K,V>> it;
        List_iterator<HashPair<K,V>> nextOccurrence();

    public:

        hash_iterator();

        hash_iterator(HashTable<K,V>* table);

        hash_iterator(const hash_iterator& it2);

        friend bool operator == <>(const hash_iterator it, const hash_iterator it2);

        friend bool operator != <>(const hash_iterator it, const hash_iterator it2);

        hash_iterator begin();

        hash_iterator end();

        hash_iterator operator ++(); //prefix

        hash_iterator operator ++( int ); //postfix

        HashPair<K,V> operator *() const;
};

template<typename K, typename V>
class HashTable
{
    protected:

        HashList<K,V>* entries;
        int m;  //table dimension
        friend class hash_iterator<K,V>;

    public:

        HashTable(const int capacity);
        //Creates a new hash table with given dimension

        ~HashTable();
        //Destructor

        bool contains(const K k) const;
        //Returns true if the hashtable contains k

        V lookup(const K k) const;
        //returns the value being searched if present, nil otherwise

        V operator [](const K k) const;
        // same as lookup, with a array like notation

        void insert(const K key,const V value) const;
        //Inserts the key-value pair into the table

        void remove(const K key) const;
        //Given a key, it removes the key-pair value, if present

        int Hash(const long int key) const;
        //Hash function

        hash_iterator<K,V> begin();

        hash_iterator<K,V> end();
};

namespace keyOnly
{

    template<typename K>
    class HashList
    {
        private:

            List<K> l;

        public:

            List_iterator<K> find(const K key) const;
            // Returns an HashPair given a key if present, null if absent

            void insert(const K key) const;
            // Inserts a key-value pair in the HashList

            void remove(const K key) const;
            // Removes an element given a key

            bool empty() const;
            // Returns true if the list is empty, false otherwise

            List_iterator<K> begin() const;

            List_iterator<K> end() const;

            bool finished(List_iterator<K> const p) const;

    };

    template<typename K>
    class HashTable;

    template<typename K>
    class hash_iterator;

    template<typename K>
    bool operator ==(const hash_iterator<K> it, const hash_iterator<K> it2);

    template<typename K>
    bool operator !=(const hash_iterator<K> it, const hash_iterator<K> it2);

    template<typename K>
    class hash_iterator
    {
        protected:

            HashTable<K>* baseTable;
            int i;
            List_iterator<K> it;
            List_iterator<K> nextOccurrence();

        public:

            hash_iterator();

            hash_iterator(HashTable<K>* table);

            hash_iterator(const hash_iterator& it2);

            friend bool operator == <>(const hash_iterator it, const hash_iterator it2);

            friend bool operator != <>(const hash_iterator it, const hash_iterator it2);

            hash_iterator begin();

            hash_iterator end() const;

            hash_iterator operator ++(); //prefix

            hash_iterator operator ++( int ); //postfix

            K operator *() const;
    };

    template<typename K>
    class HashTable
    {
        protected:

            HashList<K>* entries;
            int m;  //table dimension
            friend class hash_iterator<K>;

        public:

            HashTable();
            //Default constructor

            HashTable(const int capacity);
            //Creates a new hash table with given dimension

            ~HashTable();
            //Destructor

            bool contains(const K k) const;
            // Returns true if the table contains k

            void insert(const K key) const;
            //Inserts the key-value pair into the table

            void remove(const K key) const;
            //Given a key, it removes the key-pair value, if present

            int Hash(const long int key) const;
            //Hash function
    };

}

#include "../src/HashTable.cpp"
#endif

and here's the code:

// HashTable.cpp
#ifndef HASHTABLE_CPP
#define HASHTABLE_CPP
#include "../include/HashTable.h"
#include <cmath>

using namespace std;

template<typename K, typename V>
HashPair<K,V>::HashPair()
{ 
    key = K();
    value = V();
}

template<typename K, typename V>
HashPair<K,V>::HashPair(const K key,const V value):key(key), value(value)
{
}

template<typename K, typename V>
K HashPair<K,V>::getKey() const
{
    return key;
}
// returns the key

template<typename K, typename V>
void HashPair<K,V>::setKey(const K key)
{
    this->key = key; 
}
// sets the key

template<typename K, typename V>
V HashPair<K,V>::getValue() const
{
    return value;
}
// returns the value

template<typename K, typename V>
void HashPair<K,V>::setValue(const V v)
{
    this->value = value;
}
// sets the value

template<typename K, typename V>
List_iterator<HashPair<K,V>> HashList<K,V>::find(const K key) const
{
    bool found = false;
    List_iterator<HashPair<K,V>> e(nullptr);
    List_iterator<HashPair<K,V>> i = l.begin();

    while(!l.finished(i) && !found)
    {
        if((*i).key == key)
        {
            e = i;
            found = true;
        }

        i++;
    }

    return e;
}
// Returns an HashPair given a key if present, null if absent

template<typename K, typename V>
void HashList<K,V>::insert(const K key,const V value) const
{
    List_iterator<HashPair<K,V>> kv = find(key);
    HashPair<K,V> k(key,value);

    if (kv != List_iterator<HashPair<K,V>>(nullptr))
    {
        l.write(kv,k);
    }
    else
    {
        l.insert(k);
    }
}
// Inserts a key-value pair in the HashList

template<typename K, typename V>
V HashList<K,V>::lookup(const K key) const
{
    List_iterator<HashPair<K,V>> kv = find(key);
    V e = V();

    if (kv != List_iterator<HashPair<K,V>>(nullptr))
    {
        e = (*kv).value;
    }

    return e;
}
// Returns a reference to an HashPair value given a key if present; null otherwise

template<typename K, typename V>
void HashList<K,V>::remove(const K key) const
{
    List_iterator<HashPair<K,V>> item = find(key);
    if(item != List_iterator<HashPair<K,V>>(nullptr))
        l.remove(item);
}

template<typename K, typename V>
bool HashList<K,V>::empty() const
{
    return l.empty();
}

template<typename K, typename V>
List_iterator<HashPair<K,V>> HashList<K,V>::begin() const
{
    return l.begin();
}

template<typename K, typename V>
List_iterator<HashPair<K,V>> HashList<K,V>::end() const
{
    return l.end();
}

template<typename K, typename V>
bool HashList<K,V>::finished(List_iterator<HashPair<K,V>> const p) const
{
    return l.finished(p);
}

template<typename K, typename V>
List_iterator<HashPair<K,V>> hash_iterator<K,V>::nextOccurrence()
{
    i++;

    it = List_iterator<HashPair<K,V>>(nullptr);

    while(i < baseTable->m && it == List_iterator<HashPair<K,V>>(nullptr))
    {
        if(baseTable->entries[i].empty())
            i++;
        else
            it = baseTable->entries[i].begin();
    }

    return it;
}

template<typename K, typename V>
hash_iterator<K,V>::hash_iterator()
{
    baseTable = nullptr;
    i = -1;
    it = List_iterator<HashPair<K,V>>(nullptr);
}

template<typename K, typename V>
hash_iterator<K,V>::hash_iterator(HashTable<K,V>* table)
{
    baseTable = table;
    i = -1;
    it = List_iterator<HashPair<K,V>>(nullptr);
}

template<typename K, typename V>
hash_iterator<K,V>::hash_iterator(const hash_iterator& it2)
{
    baseTable = it2.baseTable;
    i = it2.i;
    it = it2.it;
}

template<typename K, typename V>
bool operator ==(const hash_iterator<K,V> it, const hash_iterator<K,V> it2)
{
    return (it.baseTable == it2.baseTable && it.it == it2.it);
}

template<typename K, typename V>
bool operator !=(const hash_iterator<K,V> it, const hash_iterator<K,V> it2)
{
    return !(it == it2);
}

template<typename K, typename V>
hash_iterator<K,V> hash_iterator<K,V>::begin()
{
    if(i != 0)
        i = -1;

    hash_iterator<K,V> ret(*this);

    ret.nextOccurrence();

    return ret;
}

template<typename K, typename V>
hash_iterator<K,V> hash_iterator<K,V>::end()
{
    hash_iterator<K,V> ret(*this);
    ret.i = baseTable->m;
    ret.it = List_iterator<HashPair<K,V>>(nullptr);
    return ret;
}

template<typename K, typename V>
hash_iterator<K,V> hash_iterator<K,V>::operator ++() //prefix
{
    it++;

    if (baseTable->entries[i].finished(it))
    {
        it = nextOccurrence();
    }

    return *this;
}

template<typename K, typename V>
hash_iterator<K,V> hash_iterator<K,V>::operator ++( int ) //postfix
{
    hash_iterator<K,V> oldit(*this);

    ++(*this);

    return oldit;
}

template<typename K, typename V>
HashPair<K,V> hash_iterator<K,V>::operator *() const
{
    return *it;
}

template<typename K, typename V>
HashTable<K,V>::HashTable(const int capacity)
{
    entries = new HashList<K,V> [capacity];
    m = capacity;
}
//Creates a new hash table with given dimension

template<typename K, typename V>
HashTable<K,V>::~HashTable()
{
    delete [] entries;
}
//Destructor

template<typename K, typename V>
V HashTable<K,V>::lookup(const K k) const
{
    int i = Hash(hash<K>()(k));
    V value = V();

    if (!entries[i].empty())
        value = entries[i].lookup(k);

    return value;
}
//returns the value being searched if present, nil otherwise

template<typename K,typename V>
bool HashTable<K,V>::contains(const K k) const
{
    int i = Hash(hash<K>()(k));
    if(entries[i].empty())
        return false;
    else
    {
        if(entries[i].find(k) == List_iterator<HashPair<K,V>>(nullptr))
            return false;
        else
            return true;
    }
}
//

template<typename K,typename V>
V HashTable<K,V>::operator [](const K k) const
{
    return lookup(k);
}

template<typename K, typename V>
void HashTable<K,V>::insert(const K key,const V value) const
{
    int i = Hash(hash<K>()(key));

    entries[i].insert(key,value);
}
//Inserts the key-value pair into the table

template<typename K, typename V>
void HashTable<K,V>::remove(const K key) const
{
    int k = Hash(hash<K>()(key));   

    if (!entries[k].empty())
        entries[k].remove(key);

}
//Given a key, it removes the key-pair value, if present

template<typename K, typename V>
int HashTable<K,V>::Hash(const long int key) const
{
    return abs(key) % m;
}
//Hash function

template<typename K, typename V>
hash_iterator<K,V> HashTable<K,V>::begin()
{
    hash_iterator<K,V> ret(this);

    return ret.begin();
}

template<typename K, typename V>
hash_iterator<K,V> HashTable<K,V>::end()
{
    hash_iterator<K,V> ret(this);

    return ret.end();
}

namespace keyOnly
{

    template<typename K>
    List_iterator<K> HashList<K>::find(const K key) const
    {
        bool found = false;
        List_iterator<K> e = List_iterator<K>(nullptr);

        if(!l.empty())
        {
            List_iterator<K> i = l.begin();

            while(!l.finished(i) && !found)
            {
                if(*i == key)
                {
                    e = i;
                    found = true;
                }

                i++;
            }
        }

        return e;
    }
    // Returns an HashPair given a key if present, null if absent

    template<typename K>
    void HashList<K>::insert(const K key) const
    {
        List_iterator<K> k = find(key);

        if (k == List_iterator<K>(nullptr))
        {
            l.insert(key);
        }
        else
        {
            l.write(k,key);
        }

    }
    // Inserts a key-value pair in the HashList

    /*
    template<typename K>
    K HashList<K>::lookup(K key)
    {
        List_iterator<K> k = find(key);
        K e;

        if (k != List_iterator<K>(nullptr))
            e = *k;

        return e;
    }
    // Returns a reference to an HashPair value given a key if present; null otherwise
*/
    template<typename K>
    void HashList<K>::remove(const K key) const
    {
        List_iterator<K> item = find(key);
        if(item != List_iterator<K>(nullptr))
            l.remove(item);
    }

    template<typename K>
    bool HashList<K>::empty() const
    {
        return l.empty();
    }

    template<typename K>
    List_iterator<K> HashList<K>::begin() const
    {
        return l.begin();
    }

    template<typename K>
    List_iterator<K> HashList<K>::end() const
    {
        return l.end();
    }

    template<typename K>
    bool HashList<K>::finished(const List_iterator<K> p) const
    {
        return l.finished(p);
    }

    template<typename K>
    List_iterator<K> hash_iterator<K>::nextOccurrence()
    {
        i++;

        it = List_iterator<K>(nullptr);

        while(i < baseTable->m && it == List_iterator<K>(nullptr))
        {
            if(baseTable->entries[i].empty())
                i++;
            else
                it = baseTable->entries[i].begin();
        }

        return it;
    }

    template<typename K>
    hash_iterator<K>::hash_iterator()
    {
        baseTable = nullptr;
        i = -1;
        it = List_iterator<K>(nullptr);
    }

    template<typename K>
    hash_iterator<K>::hash_iterator(HashTable<K>* table)
    {
        baseTable = table;
        i = -1;
        it = List_iterator<K>(nullptr);
    }

    template<typename K>
    hash_iterator<K>::hash_iterator(const hash_iterator& it2)
    {
        baseTable = it2.baseTable;
        i = it2.i;
        it = it2.it;
    }

    template<typename K>
    bool operator ==(const hash_iterator<K> it, const hash_iterator<K> it2)
    {
        return (it.baseTable == it2.baseTable && it.it == it2.it);
    }

    template<typename K>
    bool operator !=(const hash_iterator<K> it, const hash_iterator<K> it2)
    {
        return !(it == it2);
    }

    template<typename K>
    hash_iterator<K> hash_iterator<K>::begin()
    {
        if(i != 0)
            i = -1;

        hash_iterator<K> ret(*this);

        ret.nextOccurrence();

        return ret;
    }

    template<typename K>
    hash_iterator<K> hash_iterator<K>::end() const
    {
        hash_iterator<K> ret(*this);
        ret.i = baseTable->m;
        ret.it = List_iterator<K>(nullptr);
        return ret;
    }

    template<typename K>
    hash_iterator<K> hash_iterator<K>::operator ++() //prefix
    {
        it++;

        if (baseTable->entries[i].finished(it))
        {
            it = nextOccurrence();
        }

        return *this;
    }

    template<typename K>
    hash_iterator<K> hash_iterator<K>::operator ++( int ) //postfix
    {
        hash_iterator<K> oldit(*this);

        ++(*this);

        return oldit;
    }

    template<typename K>
    K hash_iterator<K>::operator *() const
    {

        return *it;
    }

    template<typename K>
    HashTable<K>::HashTable(const int capacity)
    {
        entries = new HashList<K> [capacity];
        m = capacity;
    }
    //Creates a new hash table with given dimension

    template<typename K>
    HashTable<K>::~HashTable()
    {
        delete [] entries;
    }
    //Destructor

    template<typename K>
    HashTable<K>::HashTable()
    {
        entries = nullptr;
        m = -1;
    }

    /*
    template<typename K>
    K HashTable<K>::lookup(K k)
    {
        K key = K();
        int i = Hash(hash<K>()(k));

        if (!entries[i].empty())
            key = entries[i].lookup(k);

        return key;
    }
    //returns the value being searched if present, nil otherwise
    */

    template<typename K>
    bool HashTable<K>::contains(const K k) const
    {
        int i = Hash(hash<K>()(k));
        if(entries[i].empty())
            return false;
        else
        {
            if(entries[i].find(k) == List_iterator<K>(nullptr))
                return false;
            else
                return true;
        }
    }

    template<typename K>
    void HashTable<K>::insert(const K key) const
    {
        int i = Hash(hash<K>()(key));

        entries[i].insert(key);
    }
    //Inserts the key-value pair into the table

    template<typename K>
    void HashTable<K>::remove(const K key) const
    {
        int k = Hash(hash<K>()(key));   

        if (!entries[k].empty())
            entries[k].remove(key);

    }
    //Given a key, it removes the key-pair value, if present

    template<typename K>
    int HashTable<K>::Hash(const long int key) const
    {
        return abs(key) % m;
    }
    //Hash function
}

#endif

It works mostly fine, but I have one big problem with this code: the quantity of repeated code. As you can see, there are two versions of the HashTable, one that stores key-value pairs and requires two template arguments and one that stores only the key and requires only one. I use the latter to implement a Set that uses a HashTable to store the elements (I don't need to store a key-value pair in this case). I wonder if there is a way to handle the template arguments in C++ without having to handle the two cases separately, as a lot of code is practically the same in both cases. I've looked into variadic template arguments, but they don't seem to be what I need.

What I would like to do is, for example, in the insert function to be able to tell if the user used one or two template arguments, and, in the first case, I would insert a key-value pair in the HashTable, in the second case just a key. I don't know if it's even possible in C++, at least my searches have not been conclusive.

Other than that, any advice on the code that doesn't have to do with this problem is very well appreciated, especially in the coding style.

Yeah, I know there are std::unordered_map and std::unordered_set that do exactly what I need. I would really like to use them, but for now I can't. I'm working on a project for uni where if I need any data structure I have to write it myself, otherwise I would be using the STL any day. Also, the List data structure used in the code has been written by me as well, you can find it, together with other data structures written by me on my GitHub page.

\$\endgroup\$
4
\$\begingroup\$

Code Review

You should use the standard types more.

HashPair   => std::pair
List       => std::list
HashTable  => std::unordered_map

OK. I see this as practice (which I think is good). But you don't need to reinvent everything. The point of this exercise was the hash table but you can use the std::list and std::pair to help you (so you don't have to reinvent everything from scratch).

HashPair

Hash pair is a property bag. There is no intrinsic state to maintain. This is a classic case of trying to turn a property bag into a class where it is not needed. Just make the members publicly visible.

Arrhhhh: getters/setters (I hate getters/setters because they destroy encapsulation and expose the inner parts of the class). But in this case we want to expose the internal workings of the class. So just have public members.

See std::pair<> for an example.

Even though I don't think you should have any methods on this class lets look at some of them:

Pass values by const reference to avoid a copy. The key is probably small but the value can potentially be anything and copying it around could be expensive:

template<typename K, typename V>
HashPair<K,V>::HashPair(const K key,const V value)
                             ^^^^^^       ^^^^^^^   Copy to parameters
    : key(key)        // Copy from parameter to key
    , value(value)    // Copy from parameter to value
{}

So here both key and value are copied twice.
The standard way looks like this:

template<typename K, typename V>
HashPair<K,V>::HashPair(K const& key, V const& value)
    : key(key)        // Copy from parameter to key
    , value(value)    // Copy from parameter to value
{}

So here both key and value are copied once only.
So in C++11 we added move semantics to try and avoid copies

template<typename K, typename V>
HashPair<K,V>::HashPair(K&& key, V&& value)
    : key(std::move(key))
    , value(std::move(value))
{}

Here Key and value are moved into place (which is generally less expensive than a copy).

If you want to go super new age you can construct the value in place:

template<typename K, typename... Args>
HashPair<K,V>::HashPair(K const& key, Args&&... value)
    : key(key)
    , value(std::forward<Args>(value)...)
{}

When you can return by refernece. This will avoid a copy:

template<typename K, typename V>
V HashPair<K,V>::getValue() const
{
    return value;   // here you are returning by value and thus
                    // causing a copy.
}

Do this:

template<typename K, typename V>
V const& HashPair<K,V>::getValue() const
{
    return value;   // here you are returning by "const" reference and thus
                    // thus allowing users to read the value.
}

Don't Specify exact types when you don't need to:

Here you say the iterator type is always a List_iterator.

    List_iterator<HashPair<K,V>> begin() const;
    List_iterator<HashPair<K,V>> end() const;

But what if you change the type of the internal container. Then you need to also go and change the type of the iterators. It is best to define the iterator types in terms of the container you are using.

    Using  ValueType       = std::pair<K, V>;
    Using  Container       = std::list<ValueType>;
    Using  iterator        = typename Container::iterator;
    Using  const_iterator  = typename Container::const_iterator;

    Container  list;
    const_iterator   cbegin() const;
    const_iterator   cend()   const;
    const_iterator   begin()  const;
    const_iterator   end()    const;
    iterator         begin();
    iterator         end();

Now if I change the list type from std::list<> to List<> none of the other code needs to change. I can even change std::pair<> to HashPair<> without any change in the rest of the code.

Standard Patter for Iterators

The standard pattern for iterators is that end() returns an iterator to one past the end of the container. While looping you can test an iterator against this value to see if you have reached then end.

You seem to have added a special method to check for this:

    bool finished(List_iterator<HashPair<K,V>> const p) const;

Don't do that:
Allow your iterators to be comparted against each other with != and ==. Then your code will look normal. Also in modern C++ the range based for assumes that iterators will work like that:

for(auto const& val: container)
{
    std::cout << value << "\n";
}

For the above code to work container must work with std::begin() and std::end() which by default call begin() and end() on the container. The values returned are considered iterators and tested against each other using !=. So the above code can be considered short hand for:

for(auto loop = std::begin(container); loop != std::end(container); ++loop)
{
    auto const& value = *loop;
    std::cout << value << "\n";
}

Use break to exit loops:

while(!l.finished(i) && !found)
{
    if((*i).key == key)
    {
        e = i;
        found = true;
    }

    i++;
}

Can be written as:

while(!l.finished(i))
{
    if((*i).key == key)
    {
        e = i;
        break;   // Note the break here.
    }
    i++;
}

Prefer -> to (*).

if((*i).key == key)

Can be written as:

if (i->key == key)

Prefer prefix increment

    i++;

Prefer to use ++i (it looks more normal in modern C++). Also with the standard implementation of pre and post increment the prefix version has a slight performance advantage for iterators (not so much integers or pointers).

Rule of 3/5 Violation

Your hash table contains an owned pointer.

    HashList<K,V>* entries;

This means it is controlling the memory managed by entries. And you correctly delete the memory in the drestructor.

But you failed to implement Copy/Move semantics of the class (or delete them). Unless you overide them in the class the compiler will generate a default copy constructor operator and copy assignment operator. These do not behave well in the presence of owned RAW pointers and need to be specifically defined or deleted.

 {
     HashTable<int, int>   x;
     HashTable<int, int>   y(x);  // Works even though you 
                                  // Did not define a copy constructor
 }
 // Problem is here. You get a double delete
 // As both the destructrs try and destroies `entries` which points 
 // at the same thing.

Questions

As you can see, there are two versions of the HashTable, one that stores key-value pairs and requires two template arguments and one that stores only the key and requires only one.

The difference between the two classes is really this bit:

    HashList<K,V>* entries;

Vs

    HashList<K>* entries;

This leads to:

    List<HashPair<K,V>> l;

Vs

    List<K> l;

So the real difference is a HashPair<K,V> Vs K.

What you need to do is abstract those types and use template partial specialization.

    // Here is the template we are going to specialize.
    template<typename K, typename V>
    struct HashTableType
    {
        using ValueType = HashPair<K,V>;
    };
    // Here is the specialization when the value type is void.
    template<typename K>
    struct HashTableType<K, void>
    {
        using ValueType = K;
    };

    // Now in our main class we can use the above two classes to get
    // the correct types.
    template<typename K, typename V>
    class HashTable
    {
         using ValueType = typename HashTableType<K, V>::ValueType;
         using ListType  = typename HashList<ValueType>;

         ListType entries;
    };
    template<typename K>
    using HashSet = HashTable<K, void>;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.