Using std::map<K,V> where V has no usable default constructor
I have a symbol table imple开发者_StackOverflowmented 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.
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 ) );
In C++11, you can also do
m.emplace( k, v );
m.emplace( piecewise_construct, make_tuple(k), make_tuple(the_constructor_arg_of_v) );
even if the copy/move constructor is not supplied.
Use map<K,V>::at()
. map<K,V>::operator []
will try to default-construct an element if the key provided does not already exist.
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).
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).
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.
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[].
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> ?
You can't distinguish between lvalue and rvalue uses of operator[]
, because it is always an lvalue expression. You don't need the V
to be default constructible if you use alternatives to []
.
For lookup, you can use at
, which throws if the key is missing, rather than default-constructing one. Or you could use find
, lower_bound
or equal_range
, which return iterators.
For assignment, you can use insert_or_assign
if you have C++17, or write a free-function equivalent:
template <typename Map, typename Value = typename Map::mapped_type, typename Key = typename Map::key_type>
void insert_or_assign(Map & map, Key && key, Value && value)
{
auto it = map.lower_bound(key);
if ((it == map.end()) || map.key_comp()(key, it->first)) {
map.emplace(it, std::forward<Key>(key), std::forward<Value>(value));
} else {
it->second = std::forward<Value>(value);
}
}
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.
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 (&&).
精彩评论