Most efficient safe way to cast std::map<int, std::shared_ptr<Base>> to std::map<int, std::shared_ptr<Derived>>
It won't give exactly the same interface, but a similar but safer idea that comes to mind is using boost::transform_iterator
to create iterators that transparently handle converting the shared_ptr
pointers within the map.
#include <memory>
#include <utility>
#include <type_traits>
#include <boost/iterator/transform_iterator.hpp>
template <class Derived, class Iterator>
class downcast_pair_iterator
: public boost::transform_iterator<
std::pair<
typename std::iterator_traits<Iterator>::value_type::first_type,
const std::shared_ptr<Derived>
> (*)(Iterator),
Iterator>
{
public:
using base_value_type = typename std::iterator_traits<Iterator>::value_type;
using key_type = const typename base_value_type::first_type;
using base_mapped_type = typename base_value_type::second_type;
using mapped_type = const std::shared_ptr<Derived>;
using value_type = std::pair<key_type, mapped_type>;
private:
template <typename T>
static T* shared_to_raw(const std::shared_ptr<T>&); // undefined
static_assert(std::is_base_of_v<
std::remove_pointer_t<
decltype(shared_to_raw(std::declval<base_mapped_type&>()))>,
Derived>);
static value_type convert(const base_value_type& pair_in)
{
return value_type(pair_in.first,
std::static_pointer_cast<Derived>(pair_in.second));
}
public:
explicit downcast_pair_iterator(Iterator iter)
: transform_iterator(iter, &convert) {}
};
template <class Derived, class Iterator>
auto make_downcast_pair_iter(Iterator iter)
{
return downcast_pair_iterator<Derived, Iterator>(iter);
}
template <class Derived, class Range>
class downcast_pair_range
{
public:
explicit downcast_pair_range(Range& c)
: source_ref(c) {}
auto begin() const {
using std::begin;
return make_downcast_pair_iter<Derived>(begin(source_ref));
}
auto end() const {
using std::end;
return make_downcast_pair_iter<Derived>(end(source_ref));
}
private:
Range& source_ref;
};
template <class Derived, class Range>
auto make_downcast_pair_range(Range& r)
{
return downcast_pair_range<Derived, Range>(r);
}
template <class Derived, class Range>
auto make_downcast_pair_range(const Range &r)
{
return downcast_pair_range<Derived, const Range>(r);
}
Then your example main
could become:
int main() {
std::map<int, std::shared_ptr<Base>> test {
{0, std::make_shared<Derived>(2, 3)},
{1, std::make_shared<Derived>(4, 5)},
{2, std::make_shared<Derived>(6, 7)}
};
for (auto&& kv : make_downcast_pair_range<Derived>(test)){
std::cout << kv.first << ": "
<< kv.second->x << ", " << kv.second->y << std::endl;
}
return 0;
}
This avoids making any second container object, and does not involve undefined behavior when used correctly. Using the transforming iterators will mostly result in the same machine code as the unsafe cast, except that a dereference does create a new shared_ptr<Derived>
object, which will involve a little reference-counting overhead. See the full working program on coliru.
In addition to using make_downcast_pair_range<Derived>(some_map)
as shown in the range-based for
above, make_downcast_pair_iterator<Derived>
can be used directly to get transforming iterators for other purposes, for example from the result of a map's find(k)
. And given a transforming iterator, you can get back to an iterator for the real map using iter.base()
, for example to pass to the map's erase(iter)
.
Of course, using the result of std::static_pointer_cast
is still undefined behavior if the pointers aren't actually pointing at Derived
objects. If there's any concern someone might use the wrong "derived" template argument when getting objects, or that the maps might somehow end up containing pointers to the wrong derived object types, you could change the downcast_pair_iterator<D, I>::convert
private function to use std::dynamic_pointer_cast
instead, then throw or abort if the result is a null pointer.
You cannot directly use the std::map constructor taking a pair of iterators because the conversion is attempting to cast from Base to Derived which cannot be done implicitly, but you can run an std::transform safely. This does involve copying but has the benefit of not being undefined behavior.
template <typename M>
static std::map<int64_t, std::shared_ptr<M>> getAll(const std::string &type) {
auto* source = getAll(type);
std::map<int64_t, std::shared_ptr<M>> castMap;
std::transform(source->begin(), source->end(), std::inserter(castMap, castMap.end()), [](auto& kv) {
return std::pair<const int, std::shared_ptr<M>>(kv.first, std::static_pointer_cast<M>(kv.second));
});
return castMap;
}