Are there any advances in recent C++ that allows for differentiating between getting and setting values via the operator[] of a class? (as Python does via __setitem__ and __getitem__)
const T& operator[](unsigned int index) const;
T& operator[](unsigned int index);
I am wrapping an std::unordered_map, and want to let my users access the data via the operator[], but also do some behind the scenes record-keeping to keep things aligned in my data structure.
Searching reveals a few answers, but they are all many years old, and I was wondering if C++ has added extra functionality in the meantime.
Assume your wrapper class implements set and get methods that perform the appropriate record keeping actions. The wrapper class can then also implement operator[] to return a result object that will delegate to one of those methods depending on how the result is used.
This is in line with the first related question you identified (Operator[] C++ Get/Set).
A simple illustration is below. Note that a const map would not be able to call set_item anyway, so the const overload of operator[] calls get_item directly.
class MapType {
...
struct Result {
MapType &map_;
KeyType key_;
Result (MapType &m, KeyType k) : map_(m), key_(k) {}
operator const ValueType & () const {
return map_.get_item(key_);
}
ValueType & operator = (ValueType rhs) {
return map_.set_item(key_, rhs);
}
};
...
const ValueType & get_item (KeyType key) const {
/* ... record keeping ... */
return map_.at(key);
}
ValueType & set_item (KeyType key, ValueType v) {
/* ... record keeping ... */
return map_[key] = v;
}
...
Result operator [] (KeyType key) { return Result(*this, key); }
const ValueType & operator [] (KeyType key) const {
return get_item(key);
}
...
};
Actually there is not such a getting operator[] and setting operator[]. There are just constant and non-constant [] operators.
When objects are large, returning by reference may save an object creation, and also gives an opportunity to change the element on that position, so it makes sense to define these operators to return by reference than by value. To make the operator available on constant objects too, you should mark the constant overload with const keyword. The idea has not been changed since old times.
BTW, the STL has slightly different approaches with regard the container type. For example if you call [] with an out of range index in a vector, it will throw, but similar call on a non-constant map will create a node with given key. To keep things a little bit more consistent, STL provides a function named at() which regardless of container class, checks the index and throws if it is out of range.
Related
I find the update operation on std::set tedious since there's no such an API on cppreference. So what I currently do is something like this:
//find element in set by iterator
Element copy = *iterator;
... // update member value on copy, varies
Set.erase(iterator);
Set.insert(copy);
Basically the iterator return by Set is a const_iterator and you can't change its value directly.
Is there a better way to do this? Or maybe I should override std::set by creating my own (which I don't know exactly how it works..)
set returns const_iterators (the standard says set<T>::iterator is const, and that set<T>::const_iterator and set<T>::iterator may in fact be the same type - see 23.2.4/6 in n3000.pdf) because it is an ordered container. If it returned a regular iterator, you'd be allowed to change the items value out from under the container, potentially altering the ordering.
Your solution is the idiomatic way to alter items in a set.
C++17 introduced extract, see Barry's answer.
If you're stuck with an older version, there are 2 ways to do this, in the easy case:
You can use mutable on the variable that are not part of the key
You can split your class in a Key Value pair (and use a std::map)
Now, the question is for the tricky case: what happens when the update actually modifies the key part of the object ? Your approach works, though I admit it's tedious.
In C++17 you can do better with extract(), thanks to P0083:
// remove element from the set, but without needing
// to copy it or deallocate it
auto node = Set.extract(iterator);
// make changes to the value in place
node.value() = 42;
// reinsert it into the set, but again without needing
// to copy or allocate
Set.insert(std::move(node));
This will avoid an extra copy of your type and an extra allocation/deallocation, and will also work with move-only types.
You can also extract by key. If the key is absent, this will return an empty node:
auto node = Set.extract(key);
if (node) // alternatively, !node.empty()
{
node.value() = 42;
Set.insert(std::move(node));
}
Update: Although the following is true as of now, the behavior is considered a defect and will be changed in the upcoming version of the standard. How very sad.
There are several points that make your question rather confusing.
Functions can return values, classes can't. std::set is a class, and therefore cannot return anything.
If you can call s.erase(iter), then iter is not a const_iterator. erase requires a non-const iterator.
All member functions of std::set that return an iterator return a non-const iterator as long as the set is non-const as well.
You are allowed to change the value of an element of a set as long as the update doesn't change the order of elements. The following code compiles and works just fine.
#include <set>
int main()
{
std::set<int> s;
s.insert(10);
s.insert(20);
std::set<int>::iterator iter = s.find(20);
// OK
*iter = 30;
// error, the following changes the order of elements
// *iter = 0;
}
If your update changes the order of elements, then you have to erase and reinsert.
You may want to use an std::map instead. Use the portion of Element that affects the ordering the key, and put all of Element as the value. There will be some minor data duplication, but you will have easier (and possibly faster) updates.
I encountered the very same issue in C++11, where indeed ::std::set<T>::iterator is constant and thus does not allow to change its contents, even if we know the transformation will not affect the < invariant. You can get around this by wrapping ::std::set into a mutable_set type or write a wrapper for the content:
template <typename T>
struct MutableWrapper {
mutable T data;
MutableWrapper(T const& data) : data(data) {}
MutableWrapper(T&& data) : data(data) {}
MutableWrapper const& operator=(T const& data) { this->data = data; }
operator T&() const { return data; }
T* operator->() const { return &data; }
friend bool operator<(MutableWrapper const& a, MutableWrapper const& b) {
return a.data < b.data;
}
friend bool operator==(MutableWrapper const& a, MutableWrapper const& b) {
return a.data == b.data;
}
friend bool operator!=(MutableWrapper const& a, MutableWrapper const& b) {
return a.data != b.data;
}
};
I find this much simpler and it works in 90% the cases without the user even noticing there to be something between the set and the actual type.
This is faster in some cases:
std::pair<std::set<int>::iterator, bool> result = Set.insert(value);
if (!result.second) {
Set.erase(result.first);
Set.insert(value);
}
If the value is usually not already in the std::set then this can have better performance.
I have a pointer to a list of pointers, as a private variable. I also have a getter that returns the pointer to the list. I need to protect it from changes.
I couldn't find how to use reinterpret_cast or const_cast on this.
class typeA{
shared_ptr<list<shared_ptr<typeB>>> l;
public:
shared_ptr<list<shared_ptr<const typeB>>> getList(){return (l);};
};
The compiler returns:
error: could not convert ‘((typeA*)this)->typeA::x’ from ‘std::shared_ptr<std::__cxx11::list<std::shared_ptr<typeB> > >’ to ‘std::shared_ptr<std::__cxx11::list<std::shared_ptr<const typeB> > >’|
||=== Build failed: 1 error(s), 0 warning(s) (0 minute(s), 0 second(s)) ===|
It seems as const shared_ptr<list<shared_ptr<typeB>>> and shared_ptr<const list<shared_ptr<typeB>>> work fine.
Is it possible to do return l as a complete const, like:
const shared_ptr<const list<shared_ptr<const typeB>>>
or at least like:
shared_ptr<list<shared_ptr<const typeB>>>
?
References instead of pointers is not an option. To declare l as shared_ptr<list<shared_ptr<const typeB>>> also is not a wanted solution.
EDIT: no 'int' anymore.
It seems as it is not possible exactly what I wanted, but the suggested solutions are good. Yes, copying pointers is acceptable.
My bad i didn't put typeB immediately. I am aware of some advantages of references over pointers, but I hoped there is some similar solution.
You can create a new list of const int's from your original list and return that:
std::shared_ptr<std::list<std::shared_ptr<const int>>> getList(){
return std::make_shared<std::list<std::shared_ptr<const int>>>(l->begin(), l->end());
}
If you want to prevent people from making changes to the returned list, make it const too:
std::shared_ptr<const std::list<std::shared_ptr<const T>>> getList(){
return std::make_shared<const std::list<std::shared_ptr<const T>>>(l->cbegin(), l->cend());
}
The shared pointer returned by this function does not point to the original list but to the newly created list.
An alternative may be to provide iterators that, when dereferenced, returns const T& (where T is the type you actually store). That way there will be no need to copy the whole list every time you want to go though it. Example:
#include <iostream>
#include <list>
#include <memory>
struct example {
int data;
example(int x) : data(x) {}
};
template <class T>
class typeA {
std::shared_ptr<std::list<std::shared_ptr<T>>> l = std::make_shared<std::list<std::shared_ptr<T>>>();
public:
template< class... Args >
void add( Args&&... args ) {
l->emplace_back(std::make_shared<T>(std::forward<Args>(args)...));
}
// a very basic iterator that can be extended as needed
struct const_iterator {
using uiterator = typename std::list<std::shared_ptr<T>>::const_iterator;
uiterator lit;
const_iterator(uiterator init) : lit(init) {}
const_iterator& operator++() { ++lit; return *this; }
const T& operator*() const { return *(*lit).get(); }
bool operator!=(const const_iterator& rhs) const { return lit != rhs.lit; }
};
const_iterator cbegin() const noexcept { return const_iterator(l->cbegin()); }
const_iterator cend() const noexcept { return const_iterator(l->cend()); }
auto begin() const noexcept { return cbegin(); }
auto end() const noexcept { return cend(); }
};
int main() {
typeA<example> apa;
apa.add(10);
apa.add(20);
apa.add(30);
for(auto& a : apa) {
// a.data = 5; // error: assignment of member ‘example::data’ in read-only object
std::cout << a.data << "\n";
}
}
When you convert a pointer-to-nonconst to a pointer-to-const, you have two pointers. Furthermore, a list of pointers-to-nonconst is a completely different type from a list of pointers-to-const.
Thus, if you want to return a pointer to a list of pointers-to-const, what you must have is a list of pointers-to-const. But you don't have such list. You have a list of pointers-to-nonconst and those list types are not interconvertible.
Of course, you could transform your pointers-to-nonconst into a list of pointers-to-const, but you must understand that it is a separate list. A pointer to former type cannot point to the latter.
So, here is an example to transform the list (I didn't test, may contain typos or mistakes):
list<shared_ptr<const int>> const_copy_of_list;
std::transform(l->begin(), l->end(), std::back_inserter(const_copy_of_list),
[](auto& ptr) {
return static_pointer_cast<const int>(ptr);
});
// or more simply as shown by Ted:
list<shared_ptr<const int>> const_copy_of_list(l->begin(), l->end());
Since we have created a completely new list, which cannot be pointed by l, it makes little sense to return a pointer. Let us return the list itself. The caller can wrap the list in shared ownership if the need it, but don't have to when it is against their needs:
list<shared_ptr<const int>> getConstCopyList() {
// ... the transorm above
return const_copy_of_list;
}
Note that while the list is separate, the pointers inside still point to the same integers.
As a side note, please consider whether shared ownership of an int object makes sense for your program - I'm assuming it is a simplification for the example.
Also reconsider whether "References instead of pointers is not an option" is a sensible requirement.
You problem squarely lies at
but I do not want to mix references and pointers. It is easier and cleaner to have just pointers.
What you are finding here is that statement is wrong. A list<TypeB> can bind a const list<TypeB> & reference, and none of the list's members will allow any modification of the TypeB objects.
class typeA {
std::vector<typeB> l;
public:
const std::vector<typeB> & getList() const { return l; };
};
If you really really must have const typeB, you could instead return a projection of l that has added const, but that wouldn't be a Container, but instead a Range (using the ranges library voted into C++20, see also its standalone implementation)
std::shared_ptr<const typeB> add_const(std::shared_ptr<typeB> ptr)
{
return { ptr, ptr.get() };
}
class typeA {
std::vector<std::shared_ptr<typeB>> l;
public:
auto getList() const { return l | std::ranges::transform(add_const); };
};
Another alternative is that you can wrap your std::shared_ptrs in something like std::experimental::propagate_const, and just directly return them.
What you have here is a VERY complex construct:
shared_ptr<list<shared_ptr<typeB>>> l;
This is three levels of indirection, of which two have reference counting lifetime management, and the third is a container (and not memory-contiguous at that).
Naturally, given this complex structure, it is not going to be easy to convert it to another type:
shared_ptr<list<shared_ptr<const typeB>>>
Notice that std::list<A> and std::list<const A> are two distinct types by design of standard library. When you want to pass around non-modifying handles to your containers, you are usually supposed to use const_iterators.
In your case there is a shared_ptr on top of the list, so you can't use iterators if you want that reference counting behavior.
At this point comes the question: do you REALLY want that behavior?
Are you expecting a situation where your typeA instance is destroyed, but you still have some other typeA instances with the same container?
Are you expecting a situation where all your typeA instances sharing the container are destroyed, but you still have some references to that container in other places of your runtime?
Are you expecting a situation where the container itself is destroyed, but you still have some references to some of the elements?
Do you have any reason at all to use std::list instead of more conventional containers to store shared pointers?
If you answer YES to all the bullet points, then to achieve your goal you'll probably have to design a new class that would behave as a holder for your shared_ptr<list<shared_ptr<typeB>>>, while only providing const access to the elements.
If, however, on one of the bullet points your answer is NO, consider redesigning the l type. I suggest starting with std::vector<typeB> and then only adding necessary modifications one by one.
The problem with templates is that for any
template <typename T>
class C { };
any two pairs C<TypeA> and C<TypeB> are totally unrelated classes – this is even the case if TypeA and TypeB only differ in const-ness.
So what you actually want to have is technically not possible. I won't present a new workaround for now, as there are already, but try to look a bit further: As denoted in comments already, you might be facing a XY problem.
Question is: What would a user do with such a list? She/he might be iterating over it – or access single elements. Then why not make your entire class look/behave like a list?
class typeA
{
// wondering pretty much why you need a shared pointer here at all!
// (instead of directly aggregating the list)
shared_ptr<list<shared_ptr<typeB>>> l;
public:
shared_ptr<list<shared_ptr<typeB>>>::const_iterator begin() { return l->begin(); }
shared_ptr<list<shared_ptr<typeB>>>::const_iterator end() { return l->end(); }
};
If you used a vector instead of a list, I'd yet provide an index operator:
shared_ptr<typeB /* const or not? */> operator[](size_t index);
Now one problem yet remains unsolved so far: The two const_iterators returned have an immutable shared pointer, but the pointee is still mutable!
This is a bit of trouble - you'll need to implement your own iterator class now:
class TypeA
{
public:
class iterator
{
std::list<std::shared_ptr<int>>::iterator i;
public:
// implementation as needed: operators, type traits, etc.
};
};
Have a look at std::iterator for a full example – be aware, though, that std::iterator is deprecated, so you'll need to implement the type-traits yourself.
The iterator tag to be used would be std::bidirectional_iterator_tag or random_access_iterator_tag (contiguous_iterator_tag with C++20), if you use a std::vector inside.
Now important is how you implement two of the needed operators:
std::shared_ptr<int const> TypeA::iterator::operator*()
{
return std::shared_ptr<int const>(*i);
}
std::shared_ptr<int const> TypeA::iterator::operator->()
{
return *this;
}
The other operators would just forward the operation to the internal iterators (increment, decrement if available, comparison, etc).
I do not claim this is the Holy Grail, the path you need to follow under all circumstances. But it is a valuable alternative worth to at least consider...
While searching for methods for overloading Subscript('[]') operator for template class, I came across two different techniques.
First Technique:
Overloading the operator [] returning pointer to the container directly, which will allow both reading value and assigning value. A sample implementation of this technique:
template <class T>
class X
{
int _size;
T *container;
public:
X(int sz)
{
_size=sz;
container=new T[sz]();
}
~X()
{
}
T& operator [](int indx)
{
return container[indx];
}
};
With main() as:
X<int> sample(100);
cout<<sample[9]<<endl;
sample[9]=9;
cout<<sample[9]<<endl;
Output:
0
9
Second Technique:
Second technique involves declaring a proxy class and overloading the operator = via that class. A sample implementation of this technique:
template <class T>
class X
{
int _size;
T *container;
public:
X(int sz)
{
_size=sz;
container=new T[sz]();
}
~X()
{
}
class Proxy
{
int indx;
X<T> &parent;
public:
Proxy(X<T> &p, int x) : parent(p),indx(x)
{
}
void operator =(T assgn)
{
parent.container[indx]=assgn;
}
operator T const &()
{
return parent.container[indx];
}
friend class X<T>;//unnecessary line, I know!
};
Proxy operator[](int indx)
{
return Proxy(*this,indx);
}
};
With same main() we get same output.
I personally like the second method. But, I really want to compare these two methods. What are the major functional difference of these two techniques. What advantages each of these method have?
The proxy-based technique you describe can be used if you want to expose a sequence of elements that are not stored as-such (requiring a conversion from and to storage) or that cannot simply be accessed by-reference. An example is std::vector < bool >: it packs eight bools in every byte (one in each bit) in storage. Storing them that way, it is not possible to return a reference to a single such bool, so the index operator returns a "proxy-object" instead to support reading and writing of the contained bools.
If you can return a direct reference to the stored objects, there is no real advantage to wrapping it in a proxy unless you want to restrict the assignment (only allow positive values in the container for example).
Usually, a proxy is used when you want to return something that doesn't match the internal storage of the data. The classic example is a 2D matrix, with elements stored in a single array. If you provide an operator to return rows or columns, you need proxies. Another example is the infamous std::vector<bool>, where the data need not be stored as a block of bool, but access must return bool to the user.
Proxies can be used to return different "views" of segments of an internal data representation. In your example, there seems no reason to use them.
Getting proxy objects right for most client usage is considerably trickier... for example - what if someone says:
tcp_peer.send_from_iterator_range(&sample[2], &sample[7+1]);
If sample::operator[] returns a temporary proxy, and that proxy doesn't carefully replace operator&, then the code asks for the addresses of the proxies themselves.
Some client usage just can't be supported without losing the Proxy's ability to intercept reads and/or writes to the data, for example in...
Container::element_type& ref = container[n];
ref = 20;
...the client code assumes the container's operator[] will yield a reference to an actual element. Any proxy returned by operator[] must either provide an operator element_type&() - handing over such a reference and taking itself out of play - or refuse to (e.g. only return a const reference, return a temporary by value to which a non-const reference can't be bound) and force edits to the client code.
So, proxy's are 95% as good when you need them, but best avoided when you don't.
I've written my own generic tree implementation, and when writing iterators for it, I am having trouble with const correctness. The issue I am having currently is as follows:
This is the header file for a DFS iterator I wrote:
template<class Item>
class DFSIterator
{
public:
DFSIterator(const Item& rRootNode);
~DFSIterator();
DFSIterator* First();
DFSIterator* operator++(int rhs);
Item* operator*() const;
Item* operator->() const;
bool isDone() const;
template <class Node> friend class Node;
private:
void initListIterator(const Item* currentNode);
bool m_bIsDone;
const Item* m_pRootNode;
const Item* m_pCurrentNode;
ListIterator<Item>* m_pCurrentListIter;
std::map<const Item*, ListIterator<Item>*> m_listMap;
};
So the bit I am concerned about is the dereference operator:
template<class Item>
Item* DFSIterator<Item>::operator*() const
{
if(isDone())
{
return NULL;
}
else
{
return const_cast<Item*>(m_pCurrentNode);
}
}
Is it appropriate to do a const_cast there? I'm wondering if this would cause problems if a user put const objects into the container?
As your constructor uses a const Item, your operator shoud return a const pointer.
If you want to return a non-const item, you should use a non const parameter for your constructor. A solution is having a base class using const objects, and a subclass working with non-const objects (A bit in a way it is done in Objc, eg NSString and NSMutableString).
Do not cast the const away, as it will break its meaning!
There is a reason why the STL has a const_iterator and iterator class. If you want const correctness you will have to implement two seperate iterator classes. One of the goals of a iterator is to mimic a save pointer. So you do not want to return null if you're beyond the iteration range, you want to raise a debug assertion if this actually happens.
A const iterator class could look something like this:
template<class Item>
class DFSIteratorConst
{
public:
DFSIteratorConst(const Item& node)
{
m_pCurrentNode = &node;
};
const Item& operator*() const
{
assert(!IsDone());
return *m_pCurrentNode;
}
void operator++()
{
assert(!IsDone());
m_pCurrentNode = m_pCurrentNode->next;
}
operator bool() const
{
return !IsDone();
}
bool IsDone() const
{
return m_pCurrentNode == nullptr;
}
private:
Item const * m_pCurrentNode;
};
A few things to note:
consider returning a reference to the node (a const reference). This causes you to not be able to return null if the iterator is past the last element. Dereferencing a past the end iterator is usually bad behaviour and you want to find those issues during debug builds and testing
the const iterator class has a pointer to a const element. Meaning that it can change the pointer (otherwise you would not be able to iterate) but can not modify the element itself
the implicit conversion to bool is practical if you want to check the iterator in a while loop. This allows you to write: while(it) { it++; } instead of while(!it.IsDone()) { it++; } again, something that resembles a classic pointer.
use nullptr if available
As I can see from your use of std::map your are already using the STL. Maybe it would be easier to use exsiting STL iterators?
const_casting in itself is always safe, but any attempt to write to const_casted values who have been declared const is undefined behaviour.
In this case, if your returned pointer points to a value that is declared const and you attempt to modify it, you'll get undefined behavior.
Rule of thumb: If you need to use const_cast for anything but interoperation with const-incorrect code, your design is broken.
The standard says in 7.1.6.1 The cv-qualifiers:
Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior.
Normally, you write two iterators versions, an iterator and a const_iterator.
There are template tricks to avoid the code duplication that is exposed in the Boost.Iterator library documentation. See how the Iterator Adaptor is defined as it is quite long.
The thing is that you will write a BaseIterator that is const conscious, and then provide aliases:
typedef BaseIterator<Item> Iterator;
typedef BaseIterator<Item const> ConstIterator;
The trickery is in how you define the conversion constructor so that Iterator to ConstIterator is possible but the reverse is not.
The real problem is that this code/interface is "lying".
The constructor says: I don't change you (the root).
But the iterator gives changeable Item away.
To be "honest" either the constructor takes a changeable root, even when it will not changed within this class,
or this class does not give changeable Item away.
However, Item itself defines whether it give changeable childs away.
Depending on that code may not compile. Maybe this is the reason for your construction.
Long talking, short meaning: this design is poor and should be changed
Probably you need two templates depending on the "const"-ness of given childs away
I'm writing a simple hash map class:
template <class K, class V> class HashMap;
The implementation is very orthodox: I have a heap array which doubles in size when it grows large. The array holds small vectors of key/value pairs.
Vector<Pair<K, V> > *buckets;
I would like to overload the subscript operator in such a way that code like this will work:
HashMap<int, int> m;
m[0] = 10; m[0] = 20;
m[2] = m[1] = m[0];
In particular,
For m[k] = v where m does not contain k, I'd like a new entry to be added.
For m[k] = v where m does contain k, I'd like the old value to be replaced.
In both of these cases, I'd like the assignment to return v.
Presumably the code will look something like
V& operator[](K &key)
{
if (contains(key))
{
// the easy case
// return a reference to the associated value
}
else
{
Vector<Pair<K, V> > *buck = buckets + hash(k) % num_buckets;
// unfinished
}
}
How should I handle the case where the key is not found? I would prefer to avoid copying values to the heap if I can.
I suppose I could make a helper class which overloads both the assignment operator and a cast to V, but surely there is a simpler solution?
Edit: I didn't realize that std::map required that the value type have a zero argument constructor. I guess I will just default-construct a value as well.
How should I handle the case where the key is not found?
Insert a new element with that key and return a reference to the value of that new element. Effectively, your pseudocode becomes something equivalent to:
if (!contains(key))
insert(Pair<K, V>(key, V()));
return reference_to_the_element_with_that_key;
This is exactly what your requirement is, too. You said "For m[k] = v where m does not contain k, I'd like a new entry to be added."
How should I handle the case where the key is not found?
std::map creates a new object, and inserts it into the map, and returns its reference. You can also do the same.
Alternatively, you can throw an exception KeyNotFoundException like the way .NET map throws. Of course, you've to define KeyNotFoundException yourself, possibly deriving from std::runtime_exception.
By the way, as a general rule, always implement operator[] in pair as:
V &operator[](const K &key);
const V &operator[](const K &key) const;
Just for the sake for const-correctness. However, if you decide to create a new object and insert it into the map, when the key is not found, then this rule is not applicable here, as const version wouldn't make sense in this situation.
See this FAQ:
What's the deal with "const-overloading"?
It sounds like what you want is a "smart reference", which you cannot generically implement in C++ because you cannot overload the dot operator (among other reasons).
In other words, instead of returning a reference to a V, you would return a "smart reference" to a V, which would contain a pointer to V. That smart reference would implement operator=(const V &v) as this->p = new V(v), which only requires a copy constructor (not a zero-argument constructor).
The problem is that the smart reference would have to behave like an actual reference in all other ways. I do not believe you can implement this in C++.
One not-quite-solution is to have your constructor take a "default" instance of V to use for initializing new entries. And it could default to V().
Like this:
template<class K, class V> class HashMap {
private:
V default_val;
public:
HashMap(const V& def = V()) : default_val(def) {
...
}
...
};
When V lacks a zero-argument constructor, HashMap h will not compile; the user will need to provide a V object whose value will be returned when a key is accessed for the first time.
This assumes V has a copy constructor, of course. But from your examples, that seems like a requirement anyway.
The simple solution is to do as std::map does: construct a new entry,
using the default constructor of the value type. This has two
drawbacks: you won't be able to use [] on a HashMap const, and you
can't instantiate HashMap with a value type which doesn't have a default
constructor. The first is more or less implicit in the specification,
which says that [] may modify the map. There are several solutions
for the second: the simplest is probably to pass an instance of a
"default" value to the constructor, which saves it, and uses it to copy
construct the new instance, e.g.:
template <typename Key, typename Value>
class HashMap
{
// ...
Value m_defaultValue;
public:
HashMap( ..., Value const& defaultValue = Value() )
: ... , m_defaultValue( defaultValue )...
Value& operator[]( Key& key )
{
// ...
// not found
insert( key, m_defaultValue );
// return reference to newly inserted value.
}
};
Alternatively, you can have operator[] return a proxy, something like:
template <typename Key, typename Value>
class HashMap::Helper // Member class of HashMap
{
HashMap* m_owner;
Key m_key;
public:
operator Value&() const
{
if ( ! m_owner->contains( m_key ) )
m_owner->createEntryWithDefaultValue( m_key );
return m_owner->getValue( m_key );
}
Helper& operator=( Value const& newValue ) const
{
m_owner->insert( m_key, newValue );
return *this;
}
};
Note that you'll still need the default value for the case where someone
writes:
v = m[x];
and x isn't present in the map. And that things like:
m[x].f();
won't work. You can only copy the entire value out or assign to it.
(Given this, I'd rather prefer the first solution in this case. There
are other cases, however, where the proxy is the only solution, and we
have to live with it.)