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"]);
}
Why do you store the value in kvAssign? This can lead to a defect in user code. Consider the following:
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.
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?
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.