2

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?

smac89
  • 39,374
  • 15
  • 132
  • 179
  • You need to access `table` via a `const` reference or pointer, otherwise the non-const overload will be chosen. – juanchopanza Sep 07 '13 at 06:41
  • Can you elaborate on that? What do you mean by `access table via const reference or pointer`? – smac89 Sep 07 '13 at 06:44
  • For example, `void foo(const HashTable& t) { std::cout << t["hash"] << "\n";`. – juanchopanza Sep 07 '13 at 06:46
  • So you are saying I need a reference to the table in order for the getter method to be called? But what if I am in main and I do something like what I've shown above? How would I overload those operators to do what I need? Or should I just overload the `=` operator? – smac89 Sep 07 '13 at 06:56
  • Your overloads are generally OK, there is no clean way around this. In the standard library, the approach is to provide only a non-const `operator []`, and provide `const` and non-`const` member functions `at()`, both of which doing range checking and throwing if the key is not found. I have added an example expanding on my first comment. – juanchopanza Sep 07 '13 at 07:01

2 Answers2

3

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.

Jerry Coffin
  • 476,176
  • 80
  • 629
  • 1,111
  • I think you forgot one more parameter in the constructor of the proxy: `proxy(HashTable const &h, const K& Key) : h(h), key(Key) {}` ? – smac89 Sep 07 '13 at 07:13
  • @Smac89: Oops -- yes, looks like I had. And actually, in this case the parameter needs to be a non-const reference too. – Jerry Coffin Sep 07 '13 at 07:16
  • What is the return type of `operator V() const`? – smac89 Sep 07 '13 at 07:24
  • @Smac89: `V`. In general, for some conversion operator `operator T`, the return type is implicitly `T`. – Jerry Coffin Sep 07 '13 at 07:26
  • Thank you for this Jerry. I am close to completion but my brain is tired so I will continue tomorrow – smac89 Sep 07 '13 at 07:52
  • I am surprised that this worked as well as it did. But my question is, if the `proxy operator []` returns a proxy object; how is it still read as `int`? Supposing I did the assignment `table["hashthis"] = 99` then I do `cout << table["hashthis"]`. How is this returning int and printing the value 99? – smac89 Sep 07 '13 at 17:14
  • Magic! Seriously, each use of `table["hashthis"]` returns a proxy object that overloads both `operator=(V)` and `operator V`. The first allows assignment of a V (in this case, `V` = `int`), and the second allows conversion of the proxy to a V. So, when you assign to the proxy, it calls `proxy::operator=(V)`, and when you use its value, it calls `proxy::operator V`. – Jerry Coffin Sep 07 '13 at 17:44
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/36992/discussion-between-smac89-and-jerry-coffin) – smac89 Sep 07 '13 at 17:48
2

When equivalent const and non-const overloads of a member function or operator are available, the non-const is chosen when the method is called on a non-const instance. The const overload will only be picked if the instance is const, or if it is accessed via a const reference or pointer:

struct Foo
{
  void foo() {}
  void foo() const {}
};

void bar(const Foo& f) { f.foo();}
void baz(const Foo* f) { f->foo(); }

int main()
{
  Foo f;
  f.foo(); // non-const overload chosen
  bar(f);  // const overload chosen
  bar(&f); // const overload chosen

  const Foo cf; // const instance
  cf.foo();     // const overload chosen

  const Foo& rf = f; // const reference
  rf.foo();          // const overload chosen
}
juanchopanza
  • 223,364
  • 34
  • 402
  • 480