Imagine you have a struct with a bunch of members, and you want to use a particular value referenced via one of its members as the key in a set, like so:
class ComplexClass {
public:
const string& name() const;
// tons of other stuff
};
struct MyStruct {
ComplexClass* c;
MoreStuff* x;
};
struct CmpMyStruct {
bool operator()(const MyStruct& lhs, const MyStruct& rhs) {
return lhs.c->name() < rhs.c->name();
}
};
typedef set<MyStruct, CmpMyStruct> MySet;
MySet my_set;
This works just fine, however now I'd like to do a lookup by a string name, but my_set.find() now takes of course a 'const MyStruct&'. If the name wasn't taken out of that ComplexClass but was instead a member of MyStruct, I could just quickly fake up an instance of MyStruct and use that:
MyStruct tmp_for_lookup;
tmp_for_lookup.name = "name_to_search"; // Doesn't work of course
MySet::iterator iter = my_set.find(tmp_for_lookup);
However, as said, that's not how it works, the name is in ComplexClass, so I'd have to at least put a mock of that in there or something.
So what I'd actually want is that the STL set wouldn't compare MyStructs but rather first "project" the key out of the MyStruct (which has type string), and then do its operations, including find(), on that. I started digging into the implementation of set/map in gcc to see how they solved that problem for the map, and was saddened to see that they actually solved it in the internal _Rb_tree, but didn't expose it, since it's not part of the standard. From gcc's stl_tree.h:
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc = allocator<_Val> >
class _Rb_tree
{
....
template<typename _Key, typename _Val, typename _KeyOfValue,
typename _Compare, typename _Alloc>
typename _Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::iterator
_Rb_tree<_Key, _Val, _KeyOfValue, _Compare, _Alloc>::
find(const _Key& __k)
And then in the stl_map.h:
typedef _Rb_tree<key_type, value_type, _Select1st<value_type>,
key_compare, _Pair_alloc_type> _Rep_type;
Note how it uses '_Select1st' to project the key out of the value_type, so find() can actually just work with the key. On the other hand the stl_set.h just uses the Identity in that case, as expected.
So I was wondering, is there a way that I'm currently missing for how I could achieve the same beauty & efficiency with the normal STL sets/maps (i.e. I absolutely don't want to use the GCC-specific _Rb_tree directly), such that I could really just do
MySet::iterator iter = my_set.find("somestring");
Note that I specifically don't want to change my_set to be a map from strings to MyStructs, i.e. I do not want to copy the string (or a reference to it) out of the ComplexClass just so I could do map<string, MyStruct>
or map<const string&, MyStruct>
instead.
This is almost more of a thought-exercise at this point, but seemed interesting :)
AFAIK, there's no way with the C++ standard library's
std::set
only, but you can usestd::find_if
with a predicateThis is a well known deficiency of the interface of
std::set
.boost::multi_index
withordered_unique
index provides the interface ofstd::set
with extrafind()
functions that take a comparable key, rather than a whole set element.See http://www.boost.org/doc/libs/1_52_0/libs/multi_index/doc/tutorial/key_extraction.html for more details.
Obviously, since boost is out of the question, and
std::set
is not intended to provide multiple indexes, you're bound to build your own extra index.(note: the idea will work, but the compiler will still complain since this is top-of-my-head).
Remember that the keys in an associative container (
set
ormap
) areconst
, because they are sorted as they are inserted and cannot be re-sorted later. So any solution that actually references the inserted object will be vulnerable to the possibility that you change the key member after inserting.So, the most general solution is to use a
map
and copy the key member.If the keys won't change, then you can use pointers as keys. Lookup using a temporary will require getting a pointer to temporary, but that's OK as
find
andoperator[]
won't keep a copy of their argument.Of course, another solution is to use a
set
of pointers and define the comparison function to access the member. Pointer-to-members can be used to generalize that so the key member is merely a template parameter to the comparison functor.http://ideone.com/GB2jfn
If you can handle the overhead of a virtual call on comparisson, you could use a trick like this:
Then, for lookup, add the following struct:
This depends on
std::set<>::find
not making a copy of the argument, which seems a reasonable assumption (but to my knowledge not explicitly guaranteed).