views:

399

answers:

8

I have a symbol table implemented as a std::map. For the value, there is no way to legitimately construct an instance of the value type via a default constructor. However if I don't provide a default constructor, I get a compiler error and if I make the constructor assert, my program compile just fine but crashes inside of map<K,V>::operator [] if I try to use it to add a new member.

Is there a way I can get C++ to disallow map[k] as an l-value at compile time (while allowing it as an r-value)?


BTW: I know I can insert into the map using Map.insert(map<K,V>::value_type(k,v)).


Edit: several people have proposed solution that amount to altering the type of the value so that the map can construct one without calling the default constructor. This has exactly the opposite result of what I want because it hides the error until later. If I were willing to have that, I could simply remove the assert from the constructor. What I Want is to make the error happen even sooner; at compile time. However, it seems that there is no way to distinguish between r-value and l-value uses of operator[] so it seems what I want can't be done so I'll just have to dispense with using it all together.

A: 

It's a bit ugly, but one way to work around this is to add a member variable that tracks whether an instance is valid or not. Your default constructor would marks an instance as being invalid but all your other constructors mark the instance as valid.

Make sure your assignment operator properly transfers the new member variable.

Modify your destructor to ignore invalid instances.

Modify all your other member functions to throw/error/assert when they operate on an invalid instance.

You can then use your object in a map and as long as you only use objects that were properly constructed, your code will work fine.

Again, this is a workaround if you want to use the STL map and are not willing to use insert and find instead of operator[].

R Samuel Klatchko
All that does is delay the problem. I want to make the probelm show up even sooner. As it happens, I don't need a flag as a default object seg-v when you try and use it.
BCS
A: 

When you use an operator override in C++, it's best to stick as closely as possible with the semantics of the operator in the default case. The semantics of the default. operator[] is replacement of an existing member in an array. It would appear that std::map bends the rules a bit. That's unfortunate, because it leads to this sort of confusion.

Note that the documentation (http://www.sgi.com/tech/stl/Map.html) for operator[] under std::map says:"Returns a reference to the object that is associated with a particular key. If the map does not already contain such an object, operator[] inserts the default object data_type()."

I'd suggest that you treat replacement and insertion differently. Unfortunately, this means you need to know which is required. That may mean doing a lookup on the map first. If performance is an issue, you might need to find an optimization where you can test for membership and insert with one lookup.

Rob deFriesse
+6  A: 

You can't make the compiler differentiate between the two uses of operator[], because they are the same thing. Operator[] returns a reference, so the assignment version is just assigning to that reference.

Personally, I never use operator[] for maps for anything but quick and dirty demo code. Use insert() and find() instead. Note that the make_pair() function makes insert easier to use:

m.insert( make_pair( k, v ) );
anon
A: 

Your V doesn't have a default constructor, so you cannot really expect std::map<K,V> std::map<K,V>::operator[] to be usable.

A std::map<K, boost::optional<V> > does have a mapped_type that is default-constructible, and likely has the semantics you want. Refer to the Boost.Optional documentation for details (you will need to be aware of them).

ariels
It is perfectly OK to use non default-constructible types with std::map - you just can't use operator[].
anon
True, thanks! Incorporated into the text.
ariels
A: 

If the value-type is not default-constructible, then operator[] just won't work for you.

What you can do, though, is to provide free functions that get and set values in a map for convenience.

E.g:

template <class K, class V>
V& get(std::map<K, V>& m, const K& k)
{
    typename std::map<K, V>::iterator it = m.find(k);
    if (it != m.end()) {
        return it->second;
    }
    throw std::range_error("Missing key");
}

template <class K, class V>
const V& get(const std::map<K, V>& m, const K& k)
{
    typename std::map<K, V>::const_iterator it = m.find(k);
    if (it != m.end()) {
        return it->second;
    }
    throw std::range_error("Missing key");
}

template <class K, class V>
void set(std::map<K, V>& m, const K& k, const V& v)
{
    std::pair<typename std::map<K, V>::iterator,bool> result = m.insert(std::make_pair(k, v));
    if (!result.second) {
        result.first->second = v;
    }
}

You might also consider a getter like dict.get(key [, default]) in Python (which returns the provided default if key is not present (but that has a usability problem in that the default always has to be constructed, even if you know that key is in the map).

UncleBens
re: the default always being constructed, that's what delegates, lazy evaluation and lambdas are for :)
BCS
A: 

you could specialize std::map for your value-type. I'm not saying it's a good idea, but it can be done. I specialized scoped_ptr<FILE>'s dtor to fclose instead of delete.

Something like:

 template<class K, class Compare, class Allocator>
 my_value_type& std::map<K,my_value_type,Compare,Allocator>::operator[](const K& k) 
 {
   //...
 }

This should allow you to insert the code you want into operator[] for your type. Unfortunately, I do not know of a way in current c++ to return only r values. In c++0x you might be able to use:

 template<class K, class Compare, class Allocator>
 my_value_type&& std::map<K,my_value_type,Compare,Allocator>::operator[](const K& k) 
 {
   //...
 }

This will return an R-value reference (&&).

KitsuneYMG
+1  A: 

Not sure why it compiles for you, I think the compiler should have caught your missing constructor.

what about using

map<K,V*>

instead of

map<K,V> ?
Omry
better than map<K,V*> would be map<K,shared_ptr<V> >
drspod
Without the constructor, it doesn't compile. As to using V*, that would be sort of counter productive as it would push error detection off till even later and I'm trying to make it happen sooner. What I'm trying to do is make the code compile for cases that will never call the default constructor and fail to compile for cases that might/could call it.
BCS
what you need would be to only generate partial code, based on what you actually need. I don't think any compiler supports this.when a template is generated, the whole code is created, not just the bits you use.
Omry
+1  A: 

Derive a new class from std::map<K,V> and create your own operator[]. Have it return a const reference, which can't be used as an l-value.

Mark Ransom
Oh, `const` that might work!
BCS