Search code examples
c++exceptionoperator-overloadinghashtablegetter-setter

Properly overloading [bracket] operator for hashtable get and set


I am trying to implement a hashtable class. The problem I am facing atm is how to properly overload the square bracket operators so that getting the value at a key from the hashtable is distinguishable from setting a key to a value.

So far here is what the class looks like:

template <typename K, typename V>
class HashTable {
    typedef pair<K, V> KeyVal;
    avl_tree <KeyVal> **TABLE;
    unsigned TABLESIZE;
    
public:
    HashTable( const unsigned & );
    V& operator [] ( const K& ); //Setter
    const V& operator [](const K&) const; //Getter
    typedef unsigned (*hashtype)(const K&);
    static hashtype Hash;
    ~HashTable();
};

And this is the implementation of each overload of the brackets:

template <typename K, typename V>
V& HashTable<K, V>::operator [] ( const K& ret ) {
    unsigned index = HashTable<K, V>::Hash(ret) % TABLESIZE;
    avl_tree <KeyVal> *ptr = AVL_TREE::find(TABLE[index], KeyVal(ret, 0));
    if ( ptr == None ) ptr = (TABLE[index] = AVL_TREE::insert(TABLE[index], KeyVal(ret, 0)));
    return ptr->data.second;
}

template <typename K, typename V>
const V& HashTable<K, V>::operator [](const K& ret) const {
    avl_tree <KeyVal> *ptr = AVL_TREE::find(TABLE[HashTable<K, V>::Hash(ret) % TABLESIZE], KeyVal(ret, 0));
    if (ptr == None) throw "Exception: [KeyError] Key not found exception.";
    return ptr->data.second;
}

Now if I do:

cout << table["hash"] << "\n"; //table declared as type HashTable<std::string, int>

I get an output of 0, but I want it to use the getter implementation of the overloaded square brackets; i.e. this should throw an exception. How do I do this?


Solution

  • The usual way to handle this situation is to have operator[] return a proxy.

    Then, for the proxy overload operator T approximately as you've done your const overload above. Overload operator= about like your non-const version.

    template <typename K, typename V>
    class HashTable {
        typedef pair<K, V> KeyVal;
        avl_tree <KeyVal> **TABLE;
        unsigned TABLESIZE;
    
        template <class K, class V>
        class proxy { 
            HashTable<K, V> &h;
            K key;
        public:
            proxy(HashTable<K, V> &h, K key) : h(h), key(key) {}
    
            operator V() const { 
                auto pos = h.find(key);
                if (pos) return *pos;
                else throw not_present();
            }
    
            proxy &operator=(V const &value) {
                h.set(key, value);
                return *this;
            }
        };
    public:
        HashTable( const unsigned & );
    
        proxy operator [] ( const K& k) { return proxy(*this, k); }
        typedef unsigned (*hashtype)(const K&);
        static hashtype Hash;
        ~HashTable();
    };
    

    You basically have two cases when you use this:

    some_hash_table[some_key] = some_value;
    
    value_type v = some_hash_table[some_key];
    

    In both cases, some_hash_table[some_key] returns an instance of proxy. In the first case, you're assigning to the proxy object, so that invokes the proxy's operator=, passing it some_value, so some_value gets added to the table with key as its key.

    In the second case, you're trying to assign an object of type proxy to a variable of type value_type. Obviously that can't be assigned directly -- but proxy::operator V returns an object of the value type for the underlying Hashtable -- so the compiler invokes that to produce a value that can be assigned to v. That, in turn, checks for the presence of the proper key in the table, and throws an exception if its not present.