I have a join function that operates on STL strings. I want to be able to apply it to to a container like this:
getFoos(const std::multimap<std::string, std::string>& map) {
return join_values(",", map.equal_range("foo"));
In other words, find all matching keys in the collection and concatenate the values into a single string with the given separator. Same thing with lower_bound()
and upper_bound()
for a range of keys, begin()
/end()
for the entire contents of the container, etc..
The closest I could get is the following:
template <typename T>
struct join_range_values : public T::const_iterator::value_type::second_type {
typedef typename T::const_iterator::value_type pair_type;
typedef typename pair_type::second_type value_type;
join_range_values(const value_type& sep) : sep(sep) { }
void operator()(const pair_type& p) {
// this function is actually more complex...
*this += sep;
*this += p.second;
}
private:
const value_type sep;
};
template <typename T>
typename T::const_iterator::value_type::second_type join_values(
const typename T::const_iterator::value_type::second_type& sep,
const std::pair<typename T::const_iterator, typename T::const_iterator>& range) {
return std::for_each(range.first, range.second, join_range_values<T>(sep));
}
(I realize that inheriting from std::string
or whatever the key/value types are is generally considered a bad idea, but I'm not overloading or overriding any functions, and I don't need a virtual destructor. I'm doing so only so that I can directly use the result of for_each
without having to define an implicit conversion operator.)
There are very similar definitions for join_range_keys
, using first_type
and p.first
in place of second_type
and p.second
. I'm assuming a similar definition will work for joining std::set
and std::multiset
keys, but I have not had any need for that.
I can apply these functions to containers with strings of various types. Any combination of map
and multimap
with any combination of string
and wstring
for the key and value types seems to work:
typedef std::multimap<std::string, std::string> NNMap;
const NNMap col;
const std::string a = join_keys<NNMap>(",", col.equal_range("foo"));
const std::string b = join_values<NNMap>(",", col.equal_range("foo"));
typedef std::multimap<std::string, std::wstring> NWMap;
const NWMap wcol;
const std::string c = join_keys<NWMap>(",", wcol.equal_range("foo"));
const std::wstring d = join_values<NWMap>(L",", wcol.equal_range("foo"));
typedef std::multimap<std::wstring, std::wstring> WWMap;
const WWMap wwcol;
const std::wstring e = join_keys<WWMap>(L",", wwcol.equal_range(L"foo"));
const std::wstring f = join_values<WWMap>(L",", wwcol.equal_range(L"foo"));
This leaves me with several questions:
- Am I missing some easier way to accomplish the same thing? The function signature especially seems overly complicated.
- Is there a way to have
join_values
automatically deduce the template parameter type so that I don't need to call it withjoin_values<MapType>
every time? - How can I refactor the
join_values
andjoin_keys
functions and functors to avoid duplicating most of the code?
I did find a slightly simpler solution based on std::accumulate
, but it seems to require two complete copy operations of the entire string for each element in the range, so it's much less efficient, as far as I can tell.
template <typename T>
struct join_value_range_accum : public T::const_iterator::value_type::second_type
{
typedef typename T::const_iterator::value_type::second_type value_type;
join_value_range_accum(const value_type& sep) : sep(sep) {}
using value_type::operator=;
value_type operator+(const typename T::const_iterator::value_type& p)
{
return *this + sep + p.second;
}
private:
const value_type sep;
};
typedef std::multimap<std::string, std::string> Map;
Map::_Pairii range = map.equal_range("foo");
std::accumulate(range.first, range.second, join_value_range_accum<Map>(","));