Home > Computers, Programming Languages > Overriding both [] and = in C++

Overriding both [] and = in C++

While creating example problems for my upcoming C/C++ Essential Training course, I’ve decided to write a simple wrapper class for SQLite. This is a useful exercise I like to include in every language course I teach, and while I’ve been using C and C++ for a very long time, I haven’t yet gotten around to writing this particular library class until now.

Of course, a similar interface already exists for the STL map<T,T> object, and under many circumstances I would likely use it. But I think it’s valuable to reinvent the wheel in this case to explore how to create a similar interface for a more complex application. I like to to reduce a problem to its simplest form, eliminating unrelated complexities, before trying to solve it. This simple example allows me to focus on the interface that I’m analyzing, so that I may apply these techniques where they’re actually needed (like in my database library).

Specifically, I want to create an interface that does this:

    kv a;
    a["foo"] = "this";
    a["bar"] = "that";
    cout << a["foo"];
    cout << a["bar"];

In the long run I’ll use a similar interface for a simple key-value database based upon SQLite. So the first step is to learn how to create the interface using something simple, such as map objects.

Simply overriding operator[] is not sufficient to solve this problem. If I just return the map object from the operator[] method I can assign to it easily enough, but that solution would use the map object’s operator= method, which will not be available in my database class. So I need to come up with another, more general, solution.

The solution I’ve chosen is to use a proxy object for the assignment. By having the operator[] method return an object from a different custom class, kvAssign, I can implement the operator= override in the kvAssign class. As a side benefit, I can have kvAssign also do the type conversion for me so that it returns a std::string object.

In the code below, notice that the kv::operator[] method returns a kvAssign object. The constructor for kvAssign takes a pointer to a kv object, along with the key and value. This makes kvAssign‘s job easy: all it has to do is set the kv value and return it as a std::string. The assignment is handled in the kvAssign::operator= method, and the type conversion in the kvAssign::operator const string& method.

The header file is bwKV.hpp:

// bwKV.hpp by Bill Weinman <http://bw.org/contact/>
// Copyright (c) 2012 The BearHeart Group, LLC

#ifndef _bwKV
#define _bwKV

#include <iostream>
#include <iterator>
#include <map>

using namespace std;

namespace bwKV {

    const string _VERSION = "1.0.1";
    const string emptyString = string();

    void test( void );
    void error( const string& );
    void msg( const string& );
    void result ( const string& k, const string& v );

    class kvAssign;

    class kv {

        typedef multimap<string, string> kv_map;    // multimap so this can be multi-purpose
        typedef kv_map::value_type kv_value;
        typedef kv_map::iterator kv_map_pointer;
        typedef kv_map::const_iterator kv_map_const_pointer;

    private:
        kv_map _kv;

    public:
        const string version( void );
        void addKV( const string& key, const string& value );
        void setKV( const string& key, const string& value );
        const string& getKVValue( const string& key ) const;
        kvAssign operator[](const string& key);
    };

    class kvAssign {
    private:
        string _key;
        kv * _kv;
        string _value;
    public:
        kvAssign(kv * const kv, const string& key, const string& value);
        kvAssign operator= ( const string& rhs );
        operator const string& ();
    };
}

#endif // _bwKV

… and the implementation file is bwKV.cpp:

// bwKV.cpp by Bill Weinman <http://bw.org/>
// Copyright (c) 2012 The BearHeart Group, LLC

#include <iostream>
#include <cerrno>
#include <exception>
#include "bwKV.hpp"

using namespace std;
using namespace bwKV;

const string kv::version( void ) {
    return _VERSION;
}

void kv::addKV( const string& key, const string& value ) {
    _kv.insert(pair<string, string>(key, value));
}

// setKV( key, value )
// adds or updates -- one value per key
void kv::setKV( const string& key, const string& value ) {
    kv_map_pointer rc = _kv.find(key);
    if(rc != _kv.end()) _kv.erase(rc);  // remove key
    addKV( key, value );
}

// getKVValue( kvm, key )
// returns associated string or empty string if not found
const string& kv::getKVValue( const string& key ) const {
    kv_map_const_pointer rc = _kv.find(key);
    if(rc == _kv.end()) return emptyString; // empty string
    else return rc->second;
}

kvAssign kv::operator[](const string& key) {
    string value = getKVValue(key);
    kvAssign a(this, key, value);
    return a;
}

// kvAssign constructor
kvAssign::kvAssign(kv * const kv, const string& key, const string& value ) {
    _kv = kv;
    _key = key;
    _value = value;
}

// kvAssign operator= (used by kv[key] = rhs)
kvAssign kvAssign::operator= ( const string& rhs ) {
    _value = rhs;
    _kv->setKV( _key, rhs );
    return *this;
}

// kvAssign type conversion
// (used by lhs = kv[key])
kvAssign::operator const string& () {
    return _value;
}

// test routines

// generic C++ error reporter
void bwKV::error( const string& emsg )
{
    string errstr;
    errstr += emsg;
    if( errno ) {
        errstr += " (" + string(strerror(errno)) + ")";
    }
    cerr << errstr << endl;
    exit(1);
}

// convenient for testing
void bwKV::msg ( const string& m ) {
    cout << m << endl;
}

void bwKV::result ( const string& k, const string& v ) {
    cout << k << " is " << v << endl;
}

void bwKV::test( void )
{
    kv kv;
    msg("addKV(one, first)");
    kv.addKV("one", "first");
    msg("setKV(two, second)");
    kv.setKV("two", "second");
    msg("kv[three] = third");
    kv["three"] = "third";
    msg("kv[four] = fourth");
    kv["four"] = "fourth";

    msg("getKVValue(one)");
    result("one", kv.getKVValue("one"));

    msg("kv[two]");
    result("two", kv["two"]);

    msg("kv[three]");
    result("three", kv["three"]);

    msg("kv[four]");
    result("four", kv["four"]);
}
Advertisement
  1. 21 January 2012 at 9:34 am | #1

    Why do you store the value in kvAssign? This can lead to a defect in user code. Consider the following:

    kv["name"] = "123";
    kvAssign a = kv["name"];
    kvAssign b = kv["name"];
    a = "456";
    cout << b; //will print "123" not the expected "456"
    

    That is, it allows you create artificial copies of the value. I know you don’t intend on assigning kvAssign to a variable and reusing it, but if people start building functions or using the auto keyword the above scenario is very possible.

    • 22 January 2012 at 1:09 pm | #2

      Yes, I see that, though as you noticed, kvAssign is meant as an intermediate structure only. There are actually a few problems with my solution. I’ll be coming back around to it at some point later in this process and I’ll post a blog entry about it. How would you solve this problem?

      • 22 January 2012 at 11:33 pm | #3

        Use strictly the “like a pointer” approach and have kvAssign hold only a pointer to the original data and the key. For efficiency you could also store a reference to the original key, rather than a copy. But since this will be an SQL wrapper string copying is not likely significant.

  1. No trackbacks yet.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.